All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opencadc.fits.slice.NDimensionalSlicer Maven / Gradle / Ivy

The newest version!
/*
 ************************************************************************
 *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
 **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
 *
 *  (c) 2020.                            (c) 2020.
 *  Government of Canada                 Gouvernement du Canada
 *  National Research Council            Conseil national de recherches
 *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
 *  All rights reserved                  Tous droits réservés
 *
 *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
 *  expressed, implied, or               énoncée, implicite ou légale,
 *  statutory, of any kind with          de quelque nature que ce
 *  respect to the software,             soit, concernant le logiciel,
 *  including without limitation         y compris sans restriction
 *  any warranty of merchantability      toute garantie de valeur
 *  or fitness for a particular          marchande ou de pertinence
 *  purpose. NRC shall not be            pour un usage particulier.
 *  liable in any event for any          Le CNRC ne pourra en aucun cas
 *  damages, whether direct or           être tenu responsable de tout
 *  indirect, special or general,        dommage, direct ou indirect,
 *  consequential or incidental,         particulier ou général,
 *  arising from the use of the          accessoire ou fortuit, résultant
 *  software.  Neither the name          de l'utilisation du logiciel. Ni
 *  of the National Research             le nom du Conseil National de
 *  Council of Canada nor the            Recherches du Canada ni les noms
 *  names of its contributors may        de ses  participants ne peuvent
 *  be used to endorse or promote        être utilisés pour approuver ou
 *  products derived from this           promouvoir les produits dérivés
 *  software without specific prior      de ce logiciel sans autorisation
 *  written permission.                  préalable et particulière
 *                                       par écrit.
 *
 *  This file is part of the             Ce fichier fait partie du projet
 *  OpenCADC project.                    OpenCADC.
 *
 *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
 *  you can redistribute it and/or       vous pouvez le redistribuer ou le
 *  modify it under the terms of         modifier suivant les termes de
 *  the GNU Affero General Public        la “GNU Affero General Public
 *  License as published by the          License” telle que publiée
 *  Free Software Foundation,            par la Free Software Foundation
 *  either version 3 of the              : soit la version 3 de cette
 *  License, or (at your option)         licence, soit (à votre gré)
 *  any later version.                   toute version ultérieure.
 *
 *  OpenCADC is distributed in the       OpenCADC est distribué
 *  hope that it will be useful,         dans l’espoir qu’il vous
 *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
 *  without even the implied             GARANTIE : sans même la garantie
 *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
 *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
 *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
 *  General Public License for           Générale Publique GNU Affero
 *  more details.                        pour plus de détails.
 *
 *  You should have received             Vous devriez avoir reçu une
 *  a copy of the GNU Affero             copie de la Licence Générale
 *  General Public License along         Publique GNU Affero avec
 *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
 *  .      pas le cas, consultez :
 *                                       .
 *
 *
 ************************************************************************
 */

package org.opencadc.fits.slice;

import ca.nrc.cadc.util.StringUtil;
import ca.nrc.cadc.wcs.exceptions.NoSuchKeywordException;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import nom.tam.fits.BasicHDU;
import nom.tam.fits.Fits;
import nom.tam.fits.FitsException;
import nom.tam.fits.FitsFactory;
import nom.tam.fits.Header;
import nom.tam.fits.HeaderCard;
import nom.tam.fits.HeaderCardException;
import nom.tam.fits.ImageData;
import nom.tam.fits.ImageHDU;
import nom.tam.fits.header.DataDescription;
import nom.tam.fits.header.IFitsHeader;
import nom.tam.fits.header.Standard;
import nom.tam.image.StreamingTileImageData;
import nom.tam.image.compression.CompressedImageTiler;
import nom.tam.image.compression.hdu.CompressedImageHDU;
import nom.tam.util.FitsOutputStream;
import nom.tam.util.RandomAccessFileIO;
import org.apache.log4j.Logger;
import org.opencadc.fits.HDUIterator;
import org.opencadc.fits.NoOverlapException;
import org.opencadc.fits.RandomAccessStorageObject;
import org.opencadc.soda.ExtensionSlice;
import org.opencadc.soda.PixelRange;
import org.opencadc.soda.server.Cutout;


/**
 * Slice out a portion of an image.  This class will support the SODA Shapes and will delegate each cutout to its
 * appropriate handler (e.g. CircleCutout, EnergyCutout, etc.).
 */
public class NDimensionalSlicer {
    private static final Logger LOGGER = Logger.getLogger(NDimensionalSlicer.class);


    public NDimensionalSlicer() {
    }

    /**
     * Perform a slice operation from the input File.  Implementors will walk through the File, skipping unwanted bytes.
     *
     * 

This implementation relies on the NOM TAM FITS API to be able to look up an HDU by its Extension Name * (EXTNAME) and, optionally, it's Extension Version (EXTVER). This may affect an * underlying Fits(InputStream) unless the InputStream can handle resetting and marking. * *

It is NOT the responsibility of this method to manage the given OutputStream. The caller will * need to close it and ensure it's open outside the bounds of this method. * * @param fitsFile The File to read bytes from. This method will not close this file. * @param cutout The cutout specification. * @param outputStream Where to write bytes to. This method will not close this stream. * @throws FitsException Any FITS related errors from the NOM TAM Fits library. * @throws IOException Reading Writing errors. * @throws NoOverlapException Client error to inform that the supplied cutout is valid but yields no results. * @throws NoSuchKeywordException Reading the FITS file failed. */ public void slice(final File fitsFile, final Cutout cutout, final OutputStream outputStream) throws FitsException, IOException, NoSuchKeywordException, NoOverlapException { slice(new RandomAccessStorageObject(fitsFile, "r"), cutout, outputStream); } /** * Perform a slice operation from the input RandomAccess. Implementors will walk through the RandomAccess, skipping * unwanted bytes. * *

This implementation relies on the NOM TAM FITS API to be able to look up an HDU by its Extension Name * (EXTNAME) and, optionally, it's Extension Version (EXTVER). This may affect an * underlying Fits(InputStream) unless the InputStream can handle resetting and marking. * *

It is NOT the responsibility of this method to manage the given OutputStream. The caller will * need to close it and ensure it's open outside the bounds of this method. * * @param randomAccessDataObject The RandomAccess object to read bytes from. This method will not close * this file. * @param cutout The cutout specification. * @param outputStream Where to write bytes to. This method will not close this stream. * @throws FitsException Any FITS related errors from the NOM TAM Fits library. * @throws IOException Reading Writing errors. * @throws NoOverlapException Client error to inform that the supplied cutout is valid but yields no results. * @throws NoSuchKeywordException Reading the FITS file failed. */ public void slice(final RandomAccessFileIO randomAccessDataObject, final Cutout cutout, final OutputStream outputStream) throws FitsException, IOException, NoOverlapException, NoSuchKeywordException { final FitsOutputStream output = new FitsOutputStream(outputStream); slice(randomAccessDataObject, cutout, output); } private void slice(final RandomAccessFileIO randomAccessDataObject, final Cutout cutout, final FitsOutputStream output) throws FitsException, IOException, NoOverlapException, NoSuchKeywordException { if (isEmpty(cutout)) { throw new IllegalStateException("No cutout specified."); } // Single Fits object for the slice. It maintains state about itself such as the current read offset. final Fits fitsInput = new Fits(randomAccessDataObject); // Reduce the requested extensions to only those that overlap. final Map> overlapHDUs = getOverlap(fitsInput, cutout); if (overlapHDUs.isEmpty()) { throw new NoOverlapException(); } else { LOGGER.debug("Found " + overlapHDUs.size() + " overlapping slices."); } // This count is available because the getOverlap call above will read through and cache the HDUs. final int hduCount = fitsInput.getNumberOfHDUs(); // The count will indicate the number of reads into the file that were needed to get to the farthest requested // HDU. This DEBUG is here to help optimize the I/O. All other getHDU() calls will use the cached list of HDUs // in the Fits object and will not need a read. LOGGER.debug("Number of reads: " + hduCount); // Read the primary header first. final BasicHDU firstHDU = fitsInput.getHDU(0); if (firstHDU == null) { throw new FitsException("Invalid FITS file (No primary HDU)."); } // MEF output is expected if the number of requested HDUs, or the number of requested values is greater than // one. final boolean mefOutput = overlapHDUs.size() > 1 || overlapHDUs.values().stream().mapToInt(List::size).sum() > 1; final boolean mefInput = hduCount > 1; // Will write out to an ArrayDataOutput stream at the end. final Fits fitsOutput = new Fits(); LOGGER.debug("\nMEF Output: " + mefOutput + "\nMEF Input: " + mefInput); // The caller is expecting more than one extension. boolean firstHDUAlreadyWritten = false; if (mefInput && mefOutput) { final boolean hasData = (firstHDU.getData() != null && firstHDU.getData().getSize() > 0); // If this primary HDU has no data, then simply add it with some minor verification. if (!hasData) { final Header primaryHeader = firstHDU.getHeader(); final Header headerCopy = copyHeader(primaryHeader); setupPrimaryHeader(headerCopy, overlapHDUs.size()); fitsOutput.addHDU(FitsFactory.hduFactory(headerCopy, new ImageData())); firstHDUAlreadyWritten = true; } // Otherwise, the primary HDU has data so treat it like any other HDU in the loop below. } // Output a simple FITS file otherwise. // Iterate the requested cutouts. Lookup the HDU for each one and use the NOM TAM Tiler to pull out an // array of primitives. for (final Map.Entry> overlap : overlapHDUs.entrySet()) { final Integer nextHDUIndex = overlap.getKey(); LOGGER.debug("Next extension slice value at extension " + nextHDUIndex); try { final BasicHDU hdu = fitsInput.getHDU(nextHDUIndex); writeSlices(hdu, overlap.getValue(), fitsOutput, mefOutput, firstHDUAlreadyWritten, overlapHDUs.size() - 1); // If it wasn't true before, it is now. firstHDUAlreadyWritten = true; } finally { try { // Flush out any buffers. output.flush(); } catch (IOException e) { LOGGER.warn("Tried to flush output.", e); } } } fitsOutput.write(output); } private void setupPrimaryHeader(final Header header, final int nextEndSize) throws HeaderCardException { final HeaderCard nextEndCard = header.findCard(DataDescription.NEXTEND); // Adjust the NEXTEND appropriately. if (nextEndCard == null) { header.addValue(DataDescription.NEXTEND, nextEndSize); } else { nextEndCard.setValue(nextEndSize); } final HeaderCard extendFlagCard = header.findCard(Standard.EXTEND); // Adjust the EXTEND appropriately. if (extendFlagCard == null) { header.addValue(Standard.EXTEND, true); } else { extendFlagCard.setValue(true); } } private void setupImageHeader(final Header header, final boolean mefOutput, final boolean firstHDUAlreadyWritten, final int nextEndSize, final int[] dimensions) throws FitsException { header.deleteKey(Standard.SIMPLE); header.deleteKey(Standard.XTENSION); header.deleteKey(Standard.EXTEND); header.deleteKey(Standard.TFIELDS); if (mefOutput) { if (firstHDUAlreadyWritten) { header.addValue(Standard.XTENSION, Standard.XTENSION_IMAGE); } else { header.addValue(Standard.SIMPLE, Boolean.TRUE); setupPrimaryHeader(header, nextEndSize); } } else { // MEF input to simple output. header.addValue(Standard.SIMPLE, Boolean.TRUE); } // Values set as per FITS standard for IMAGE and SIMPLE extensions. header.addValue(Standard.PCOUNT, 0); header.addValue(Standard.GCOUNT, 1); } private void writeSlices(final BasicHDU hdu, List extensionSliceList, final Fits fitsOutput, final boolean mefOutput, final boolean firstHDUAlreadyWritten, final int nextEndSize) throws FitsException, NoOverlapException { final int[] dimensions; final Header header; if (hdu instanceof CompressedImageHDU) { final CompressedImageHDU compressedImageHDU = (CompressedImageHDU) hdu; // Follow the ZNAXISn values, if present. dimensions = compressedImageHDU.getImageAxes(); header = compressedImageHDU.getImageHeader(); final Header originalHeader = compressedImageHDU.getHeader(); // EXTNAME and EXTVER aren't copied for some reason. if (originalHeader.containsKey(Standard.EXTNAME) && !originalHeader.getStringValue(Standard.EXTNAME).equals("COMPRESSED_IMAGE")) { header.addValue(Standard.EXTNAME, originalHeader.getStringValue(Standard.EXTNAME)); } if (originalHeader.containsKey(Standard.EXTVER)) { header.addValue(Standard.EXTVER, originalHeader.getIntValue(Standard.EXTNAME)); } } else { dimensions = hdu.getAxes(); header = hdu.getHeader(); } LOGGER.debug("Writing slices with dimensions of " + Arrays.toString(dimensions)); for (final ExtensionSlice extensionSliceValue : extensionSliceList) { if (extensionSliceValue.getPixelRanges().isEmpty()) { fitsOutput.addHDU(hdu); } else if (dimensions == null) { throw new NoOverlapException(); } else { final int dimensionLength = dimensions.length; final int[] corners = new int[dimensionLength]; final int[] lengths = new int[dimensionLength]; final int[] steps = new int[dimensionLength]; // We copy the header to preserve as much as we can, then we'll remove whatever is inappropriate. final Header headerCopy = copyHeader(header); fillCornersAndLengths(dimensions, headerCopy, extensionSliceValue, corners, lengths, steps); // The data contained in this HDU cannot be used to slice from. if (corners.length == 0) { throw new NoOverlapException(); } LOGGER.debug("Tiling out " + Arrays.toString(lengths) + " at corner " + Arrays.toString(corners) + " from extension " + hdu.getTrimmedString(Standard.EXTNAME) + "," + header.getIntValue(Standard.EXTVER, 1)); // Adjust WCS headers as needed. WCSCutoutUtil.adjustHeaders(headerCopy, dimensionLength, corners, steps); setupImageHeader(headerCopy, mefOutput, firstHDUAlreadyWritten, nextEndSize, dimensions); final StreamingTileImageData streamingTileImageData; if (hdu instanceof CompressedImageHDU) { final CompressedImageHDU compressedImageHDU = (CompressedImageHDU) hdu; final CompressedImageTiler compressedImageTiler = new CompressedImageTiler(compressedImageHDU); streamingTileImageData = new StreamingTileImageData(headerCopy, compressedImageTiler, corners, lengths, steps); } else { // Assume ImageHDU streamingTileImageData = new StreamingTileImageData(headerCopy, ((ImageHDU) hdu).getTiler(), corners, lengths, steps); } fitsOutput.addHDU(new ImageHDU(headerCopy, streamingTileImageData)); } } } private boolean isEmpty(final Cutout cutout) { return cutout.pos == null && cutout.band == null && cutout.time == null && (cutout.pol == null || cutout.pol.isEmpty()) && cutout.custom == null && cutout.customAxis == null && (cutout.pixelCutouts == null || cutout.pixelCutouts.isEmpty()); } /** * Populate the corners and lengths of the tile to pull. This method will fill the corners, * lengths, and steps arrays to be used by the FITS Tiler. * * @param dimensions The dimension values to pad with. * @param header The Header to set NAXIS values for as they are calculated. * @param extensionSliceValue The requested cutout. * @param corners The corners array to indicate starting pixel points. * @param lengths The lengths of each dimension to cutout. * @param steps For striding, these values will be something other than 1. */ private void fillCornersAndLengths(final int[] dimensions, final Header header, final ExtensionSlice extensionSliceValue, final int[] corners, final int[] lengths, final int[] steps) throws FitsException { LOGGER.debug("Full dimensions are " + Arrays.toString(dimensions)); final int dimensionLength = dimensions.length; // Pad the bounds with the full dimensions as necessary. for (int i = 0; i < dimensionLength; i++) { // Need to pull values in reverse order as the dimensions (axes) are delivered in reverse order. final int maxRegionSize = dimensions[dimensionLength - i - 1]; final List pixelRanges = extensionSliceValue.getPixelRanges(); final int rangeSize = pixelRanges.size(); final PixelRange pixelRange; if (rangeSize > i) { pixelRange = pixelRanges.get(i); } else { pixelRange = new PixelRange(0, maxRegionSize); } final int rangeLowBound = pixelRange.lowerBound; final int rangeUpBound = pixelRange.upperBound; final int rangeStep = pixelRange.step; final int lowerBound = rangeLowBound > 0 ? rangeLowBound - 1 : rangeLowBound; LOGGER.debug("Set lowerBound to " + lowerBound + " from rangeLowBound " + rangeLowBound); final int upperBound; final int step; if (lowerBound > rangeUpBound) { upperBound = rangeUpBound - 2; step = rangeStep * -1; } else { upperBound = rangeUpBound; step = rangeStep; } final int nextLength = Math.min((upperBound - lowerBound), maxRegionSize); LOGGER.debug("Length is " + nextLength + " (" + upperBound + " - " + lowerBound + "):" + step); // Adjust the NAXISn header appropriately. If the step value does not divide perfectly into the length, // then there will be an extra write, so add 1 where necessary. header.addValue(Standard.NAXISn.n(i + 1), (nextLength / step) + ((nextLength % step) == 0 ? 0 : 1)); // Need to set the values backwards (reverse order) to match the dimensions. corners[corners.length - i - 1] = lowerBound; // Need to set the values backwards (reverse order) to match the dimensions. lengths[lengths.length - i - 1] = nextLength; // Need to set the values backwards (reverse order) to match the dimensions. steps[steps.length - i - 1] = step; } } /** * Make a copy of the header. Adjusting the source one directly with an underlying File will result in the source * file being modified. * * @param source The source Header. * @return Header object with reproduced cards. Never null. * @throws HeaderCardException Any I/O with Header Cards. */ private Header copyHeader(final Header source) throws HeaderCardException { final Header destination = new Header(); for (final Iterator headerCardIterator = source.iterator(); headerCardIterator.hasNext(); ) { final HeaderCard headerCard = headerCardIterator.next(); final String headerCardKey = headerCard.getKey(); final Class valueType = headerCard.valueType(); // Check for blank lines or just plain comments that are not standard FITS comments. if (!StringUtil.hasText(headerCardKey)) { destination.addValue(headerCardKey, (String) null, headerCard.getComment()); } else if (Standard.COMMENT.key().equals(headerCardKey)) { destination.insertComment(headerCard.getComment()); } else if (Standard.HISTORY.key().equals(headerCardKey)) { destination.insertHistory(headerCard.getComment()); } else { if (valueType == String.class || valueType == null) { destination.addValue(headerCardKey, headerCard.getValue(), headerCard.getComment()); } else if (valueType == Boolean.class) { destination.addValue(headerCardKey, Boolean.parseBoolean(headerCard.getValue()), headerCard.getComment()); } else if (valueType == Integer.class || valueType == BigInteger.class) { destination.addValue(headerCardKey, new BigInteger(headerCard.getValue()), headerCard.getComment()); } else if (valueType == Long.class) { destination.addValue(headerCardKey, Long.parseLong(headerCard.getValue()), headerCard.getComment()); } else if (valueType == Double.class) { destination.addValue(headerCardKey, Double.parseDouble(headerCard.getValue()), headerCard.getComment()); } else if (valueType == BigDecimal.class) { destination.addValue(headerCardKey, new BigDecimal(headerCard.getValue()), headerCard.getComment()); } else if (valueType == Float.class) { destination.addValue(headerCardKey, Float.parseFloat(headerCard.getValue()), headerCard.getComment()); } } } return destination; } private void addHeaderCard(final Header destination, final IFitsHeader headerCard, final Class valueType, final String value) throws FitsException { final String headerCardKey = headerCard.key(); // Check for blank lines or just plain comments that are not standard FITS comments. if (!StringUtil.hasText(headerCardKey)) { destination.addValue(headerCardKey, (String) null, headerCard.comment()); } else if (Standard.COMMENT.key().equals(headerCardKey)) { destination.insertComment(headerCard.comment()); } else if (Standard.HISTORY.key().equals(headerCardKey)) { destination.insertHistory(headerCard.comment()); } else { if (valueType == String.class || valueType == null) { destination.addValue(headerCardKey, value, headerCard.comment()); } else if (valueType == Boolean.class) { destination.addValue(headerCardKey, Boolean.parseBoolean(value), headerCard.comment()); } else if (valueType == Integer.class || valueType == BigInteger.class) { destination.addValue(headerCardKey, new BigInteger(value), headerCard.comment()); } else if (valueType == Long.class) { destination.addValue(headerCardKey, Long.parseLong(value), headerCard.comment()); } else if (valueType == Double.class) { destination.addValue(headerCardKey, Double.parseDouble(value), headerCard.comment()); } else if (valueType == BigDecimal.class) { destination.addValue(headerCardKey, new BigDecimal(value), headerCard.comment()); } else if (valueType == Float.class) { destination.addValue(headerCardKey, Float.parseFloat(value), headerCard.comment()); } } } private boolean matchHDU(final BasicHDU hdu, final String extensionName, final Integer extensionVersion) { final String extName = hdu.getTrimmedString(Standard.EXTNAME); // Only carry on if this HDU has an EXTNAME value. if (extName != null) { // FITS dictates the default EXTVER is 1 if not present. // https://heasarc.gsfc.nasa.gov/docs/fcg/standard_dict.html // final int extVer = hdu.getHeader().getIntValue(Standard.EXTVER, 1); // Ensure the extension name matches as that's a requirement. By default the extVer value will be 1, // which will match if no extensionVersion was requested. Otherwise, ensure the extVer matches the // requested value. Boxing extVer into a new Integer() alleviates a NullPointerException from possibly // occurring. return extName.equalsIgnoreCase(extensionName) && (((extensionVersion == null) && (extVer == 1)) || (Integer.valueOf(extVer).equals(extensionVersion))); } return false; } private void mapOverlap(final Header header, final Cutout cutout, final int hduIndex, final Map> overlapHDUIndexesSlices) throws HeaderCardException, NoSuchKeywordException { final PixelRange[] pixelCutoutBounds = WCSCutoutUtil.getBounds(header, cutout); if (pixelCutoutBounds.length > 0) { final ExtensionSlice overlapSlice = new ExtensionSlice(hduIndex); overlapSlice.getPixelRanges().addAll(Arrays.asList(pixelCutoutBounds)); final List overlapSlices = overlapHDUIndexesSlices.containsKey(hduIndex) ? overlapHDUIndexesSlices.get(hduIndex) : new ArrayList<>(); overlapSlices.add(overlapSlice); overlapHDUIndexesSlices.put(hduIndex, overlapSlices); } } // find the subset of slices that overlap the specified HDU; there can be multiple // because the pixel cutout API allows for multiple cutouts (pixel ranges) to be extracted // from a single extension // TODO: if the requested slices contain a mix of extensionName- and extensionIndex-specified // slices, then two of the slices could specify/generate the exact same (duplicate) output (if the // pixelrange(s) are the same or absent private int mapOverlap(final BasicHDU hdu, final int hduIndex, final List extensionSlices, final Map> overlapHDUIndexesSlices) throws FitsException { final List overlappingSlices = new ArrayList<>(); if (hdu != null) { for (final ExtensionSlice slice : extensionSlices) { if (matchHDU(hdu, slice.extensionName, slice.extensionVersion) || ((slice.extensionIndex != null) && (hduIndex == slice.extensionIndex))) { // Entire extension requested, so it matters not that it may not be an Image HDU. if (slice.getPixelRanges().isEmpty()) { overlappingSlices.add(slice); } else if (!(hdu instanceof ImageHDU) && !(hdu instanceof CompressedImageHDU)) { throw new UnsupportedOperationException( "Unable to slice from HDU of type: " + hdu.getClass().getSimpleName()); } else { final int[] dims; if (hdu instanceof CompressedImageHDU) { // Follow the ZNAXISn values, if present. dims = ((CompressedImageHDU) hdu).getImageAxes(); } else { dims = hdu.getAxes(); } // We need to reverse this as it comes back from nom-tam-fits final int[] dimensions = IntStream.range(0, dims.length).map(i -> dims[dims.length - i - 1]).toArray(); LOGGER.debug("Dimensions are " + Arrays.toString(dimensions)); final PixelCutout pixelCutout = new PixelCutout(dimensions); final ExtensionSlice overlapSlice = getOverlap(slice, pixelCutout); if (overlapSlice != null) { overlappingSlices.add(overlapSlice); } } } } } if (overlappingSlices.size() > 0) { overlapHDUIndexesSlices.put(hduIndex, overlappingSlices); } return overlappingSlices.size(); } /** * Obtain the overlapping indexes of matching HDUs. This will return a unique list of Integers in file order, * rather than the order that they were requested. * * @param fits The Fits object to scan. * @param cutout The requested cutout. * @return An Map of overlapping hduIndex->slice[], or empty Map. Never null. * @throws FitsException if the header could not be read */ private Map> getOverlap(final Fits fits, final Cutout cutout) throws FitsException, NoOverlapException, NoSuchKeywordException { if ((cutout.pixelCutouts != null) && !cutout.pixelCutouts.isEmpty()) { return getOverlap(fits, cutout.pixelCutouts); } else { // A Set is used to eliminate duplicates from the inner loop below. final Map> overlapHDUIndexesSlices = new LinkedHashMap<>(); // Walk through the cache first. int hduIndex = 0; for (final HDUIterator hduIterator = new HDUIterator(fits); hduIterator.hasNext(); ) { final BasicHDU hdu = hduIterator.next(); final Header header = hdu.getHeader(); mapOverlap(header, cutout, hduIndex, overlapHDUIndexesSlices); hduIndex++; } return overlapHDUIndexesSlices; } } private Map> getOverlap(final Fits fits, final List extensionSlices) throws FitsException, NoOverlapException { // A Set is used to eliminate duplicates from the inner loop below. final Map> overlapHDUIndexesSlices = new LinkedHashMap<>(); int matchCount = 0; int hduIndex = 0; for (final HDUIterator hduIterator = new HDUIterator(fits); hduIterator.hasNext() && matchCount < extensionSlices.size(); ) { final BasicHDU hdu = hduIterator.next(); matchCount += mapOverlap(hdu, hduIndex, extensionSlices, overlapHDUIndexesSlices); hduIndex++; } // Check for missing matches. final List matchedValues = overlapHDUIndexesSlices.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); final List containsAll = extensionSlices.stream().filter(e -> { boolean contained = false; for (final ExtensionSlice extensionSlice : matchedValues) { final List matchedPixelRange = extensionSlice.getPixelRanges(); final List requestedPixelRange = e.getPixelRanges(); LOGGER.debug("\nMatched: " + matchedPixelRange + "\nRequested: " + requestedPixelRange); if ((matchedPixelRange.isEmpty() && requestedPixelRange.isEmpty()) || new HashSet<>(requestedPixelRange).containsAll(matchedPixelRange)) { contained = true; break; } } return !contained; }).collect(Collectors.toList()); if (!containsAll.isEmpty()) { throw new NoOverlapException("One or more requested slices could not be found:\n" + containsAll); } return overlapHDUIndexesSlices; } private ExtensionSlice getOverlap(final ExtensionSlice extensionSlice, final PixelCutout pixelCutout) { final PixelRange[] pixelCutoutBounds = pixelCutout.getBounds(extensionSlice); if (pixelCutoutBounds == null) { return null; } else { final ExtensionSlice overlapSlice = extensionSlice.extensionIndex == null ? new ExtensionSlice(extensionSlice.extensionName, extensionSlice.extensionVersion) : new ExtensionSlice(extensionSlice.extensionIndex); overlapSlice.getPixelRanges().addAll(Arrays.asList(pixelCutoutBounds)); return overlapSlice; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy