org.esa.nest.gpf.SliceAssemblyOp.java Source code

Java tutorial

Introduction

Here is the source code for org.esa.nest.gpf.SliceAssemblyOp.java

Source

/*
 * Copyright (C) 2014 by Array Systems Computing Inc. http://www.array.ca
 *
 * 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 3 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/
 */
package org.esa.nest.gpf;

import com.bc.ceres.core.ProgressMonitor;
import com.bc.jexp.ParseException;
import com.bc.jexp.Parser;
import com.bc.jexp.Term;
import com.bc.jexp.WritableNamespace;
import com.bc.jexp.impl.ParserImpl;
import org.apache.commons.collections.map.HashedMap;
import org.esa.beam.framework.datamodel.*;
import org.esa.beam.framework.dataop.barithm.BandArithmetic;
import org.esa.beam.framework.dataop.barithm.RasterDataSymbol;
import org.esa.beam.framework.dataop.maptransf.Datum;
import org.esa.beam.framework.gpf.Operator;
import org.esa.beam.framework.gpf.OperatorException;
import org.esa.beam.framework.gpf.OperatorSpi;
import org.esa.beam.framework.gpf.Tile;
import org.esa.beam.framework.gpf.annotations.OperatorMetadata;
import org.esa.beam.framework.gpf.annotations.Parameter;
import org.esa.beam.framework.gpf.annotations.SourceProducts;
import org.esa.beam.framework.gpf.annotations.TargetProduct;
import org.esa.beam.util.ProductUtils;
import org.esa.snap.datamodel.AbstractMetadata;
import org.esa.snap.datamodel.OrbitStateVector;
import org.esa.snap.datamodel.Unit;
import org.esa.snap.eo.Constants;
import org.esa.snap.gpf.InputProductValidator;
import org.esa.snap.gpf.OperatorUtils;
import org.esa.snap.gpf.ReaderUtils;
import org.esa.snap.gpf.TileIndex;

import java.awt.*;
import java.util.*;
import java.util.List;

import static org.esa.nest.dataio.sentinel1.Sentinel1Level1Directory.getListInEvenlySpacedGrid;

/**
 * Merges Sentinel-1 slice products
 */
@OperatorMetadata(alias = "SliceAssembly", category = "SAR Processing/Sentinel-1", authors = "Jun Lu, Luis Veci", copyright = "Copyright (C) 2014 by Array Systems Computing Inc.", description = "Merges Sentinel-1 slice products")
public final class SliceAssemblyOp extends Operator {

    @SourceProducts
    private Product[] sourceProducts;
    @TargetProduct
    private Product targetProduct;

    // Only bands whose polarization is selected will be in the output product.
    @Parameter(description = "The list of polarisations", label = "Polarisations")
    private String[] selectedPolarisations;

    private MetadataElement absRoot = null;

    // The slice products will be in order in the array: 1st (top) slice is the 1st element in the array followed by
    // 2nd slice and so on.
    private Product[] sliceProducts;
    private Map<Band, BandLines[]> bandLineMap = new HashMap<>();

    // This is the raster width and height of the target product
    private int targetWidth = 0, targetHeight = 0;

    // Map a swath such as "IW1" to the assembled image height and width.
    // For GRD, use "" for swath.
    // height is 1st element. width is 2nd element.
    private Map<String, int[]> swathAssembledImageDimMap = new HashMap<>();

    // Map a product and swath such as "IW1" to the image height and width.
    // For GRD, use "" for swath.
    // height is 1st element. width is 2nd element.
    private Map<Product, Map<String, int[]>> sliceSwathImageDimMap = new HashMap<>();

    private Map<String, TiePointGeoCoding> swathGeocodingMap = new HashMap<>();

    /**
     * Default constructor. The graph processing framework
     * requires that an operator has a default constructor.
     */
    public SliceAssemblyOp() {
    }

    /**
     * Initializes this operator and sets the one and only target product.
     * <p>The target product can be either defined by a field of type {@link org.esa.beam.framework.datamodel.Product} annotated with the
     * {@link org.esa.beam.framework.gpf.annotations.TargetProduct TargetProduct} annotation or
     * by calling {@link #setTargetProduct} method.</p>
     * <p>The framework calls this method after it has created this operator.
     * Any client code that must be performed before computation of tile data
     * should be placed here.</p>
     *
     * @throws org.esa.beam.framework.gpf.OperatorException If an error occurs during operator initialisation.
     * @see #getTargetProduct()
     */
    @Override
    public void initialize() throws OperatorException {

        try {
            for (Product srcProduct : sourceProducts) {
                final InputProductValidator validator = new InputProductValidator(srcProduct);
                validator.checkIfSentinel1Product();
                validator.checkProductType(new String[] { "SLC", "GRD" });
                validator.checkAcquisitionMode(new String[] { "SM", "IW", "EW" });
            }

            sliceProducts = determineSliceProducts();

            absRoot = AbstractMetadata.getAbstractedMetadata(sliceProducts[0]);

            if (selectedPolarisations == null || selectedPolarisations.length == 0) {
                final Sentinel1Utils su = new Sentinel1Utils(sliceProducts[0]);
                selectedPolarisations = su.getPolarizations();
            }

            checkSlantRangeTimes();

            createTargetProduct();

            updateTargetProductMetadata();

            determineBandStartEndTimes();
        } catch (Throwable e) {
            OperatorUtils.catchOperatorException(getId(), e);
        }
    }

    private Product[] determineSliceProducts() throws Exception {
        if (sourceProducts.length < 2) {
            throw new Exception("Slice assembly requires at least two consecutive slice products");
        }

        final TreeMap<Integer, Product> productSet = new TreeMap<>();
        for (Product srcProduct : sourceProducts) {
            final MetadataElement origMetaRoot = AbstractMetadata.getOriginalProductMetadata(srcProduct);
            final MetadataElement generalProductInformation = getGeneralProductInformation(origMetaRoot);
            if (!isSliceProduct(generalProductInformation)) {
                throw new Exception(srcProduct.getName() + " is not a slice product");
            }

            final int totalSlices = generalProductInformation.getAttributeInt("totalSlices");
            final int sliceNumber = generalProductInformation.getAttributeInt("sliceNumber");
            //System.out.println("SliceAssemblyOp.determineSliceProducts: totalSlices = " + totalSlices + "; slice product name = " + srcProduct.getName() + "; prod type = " + srcProduct.getProductType() + "; sliceNumber = " + sliceNumber);

            productSet.put(sliceNumber, srcProduct);
        }

        //check if consecutive
        Integer prev = productSet.firstKey();
        // Note that "The set's iterator returns the keys in ascending order".
        for (Integer i : productSet.keySet()) {
            if (!i.equals(prev)) {
                if (!prev.equals(i - 1)) {
                    throw new Exception("Products are not consecutive slices");
                }
                prev = i;
            }
        }

        // Note that "If productSet makes any guarantees as to what order its elements
        // are returned by its iterator, toArray() must return the elements in
        // the same order".
        return productSet.values().toArray(new Product[productSet.size()]);
    }

    private static MetadataElement getGeneralProductInformation(final MetadataElement origMetaRoot) {
        final MetadataElement XFDU = origMetaRoot.getElement("XFDU");
        final MetadataElement metadataSection = XFDU.getElement("metadataSection");

        final MetadataElement metadataObject = findElementByID(metadataSection, "ID", "generalProductInformation");
        final MetadataElement metadataWrap = metadataObject.getElement("metadataWrap");
        final MetadataElement xmlData = metadataWrap.getElement("xmlData");
        MetadataElement generalProductInformation = xmlData.getElement("generalProductInformation");
        if (generalProductInformation == null)
            generalProductInformation = xmlData.getElement("standAloneProductInformation");
        return generalProductInformation;
    }

    private static boolean isSliceProduct(final MetadataElement generalProductInformation) {
        final String sliceProductFlag = generalProductInformation.getAttributeString("sliceProductFlag");
        return sliceProductFlag.equals("true");
    }

    private static MetadataElement findElementByID(final MetadataElement metadataSection, final String tag,
            final String id) {
        final MetadataElement[] metadataObjectList = metadataSection.getElements();

        for (MetadataElement metadataObject : metadataObjectList) {
            final String attrib = metadataObject.getAttributeString(tag, null);
            if (attrib.equals(id)) {
                return metadataObject;
            }
        }
        return null;
    }

    private String extractSwathIdentifier(final String mdsName) {
        // Includes possibly the "-".
        // E.g., it can be "iw-" or "iw1"
        return mdsName.substring(4, 7);
    }

    private String getSlantRangeTime(final Product product, final String sss) {

        final MetadataElement origProdRoot = AbstractMetadata.getOriginalProductMetadata(product);
        final MetadataElement annotation = origProdRoot.getElement("annotation");
        final MetadataElement[] annotationElems = annotation.getElements();

        String slantRangeTime = "";

        for (MetadataElement e : annotationElems) {
            if (extractSwathIdentifier(e.getName()).equals(sss)) {
                MetadataElement prod = e.getElement("product");
                MetadataElement imgAnno = prod.getElement("imageAnnotation");
                MetadataElement imgInfo = imgAnno.getElement("imageInformation");
                slantRangeTime = imgInfo.getAttributeString("slantRangeTime");
                break;
            }
        }

        //System.out.println("return slant range time for " + sss + " = " + slantRangeTime);
        return slantRangeTime;
    }

    private void checkSlantRangeTimes() {

        final Product firstSliceProduct = sliceProducts[0];
        final MetadataElement origProdRoot = AbstractMetadata.getOriginalProductMetadata(firstSliceProduct);
        final MetadataElement annotation = origProdRoot.getElement("annotation");
        final MetadataElement[] annotationElems = annotation.getElements();

        // There is some redundancy here.
        // E.g. we will check for both
        // s1a-iw-grd-vh-20140920t050131-20140920t050156-002471-002aec-002.xml and
        // s1a-iw-grd-vv-20140920t050131-20140920t050156-002471-002aec-001.xml
        for (MetadataElement e : annotationElems) {
            final String sss = extractSwathIdentifier(e.getName());
            final String slantRangeTime = getSlantRangeTime(firstSliceProduct, sss);
            //System.out.println("Check slant range time for " + e.getName() + " " + sss + " = " + slantRangeTime);
            for (int i = 1; i < sliceProducts.length; i++) {
                if (!slantRangeTime.equals(getSlantRangeTime(sliceProducts[i], sss))) {
                    throw new OperatorException("Slant range time don't agree: " + i + " " + sss);
                }
            }
        }
    }

    private ArrayList<String> getSwaths(final Product product) {

        ArrayList<String> swaths = new ArrayList<>();

        final MetadataElement origProdRoot = AbstractMetadata.getOriginalProductMetadata(product);
        final MetadataElement annotation = origProdRoot.getElement("annotation");
        final MetadataElement[] annotationElems = annotation.getElements();

        for (MetadataElement e : annotationElems) {
            String sss = extractSwathIdentifier(e.getName()).toUpperCase();
            if (sss.endsWith("-")) {
                sss = sss.substring(0, sss.length() - 1);
            }
            if (!swaths.contains(sss)) {
                swaths.add(sss);
            }
        }

        return swaths;
    }

    private void getSwathDim(final Product product, final String swath, final int[] dim) {

        // dim[0] is height; dim[1] is width

        final Band[] bands = product.getBands();

        for (Band b : bands) {
            if (b.getName().contains(swath)) {
                dim[0] = b.getSceneRasterHeight();
                dim[1] = b.getSceneRasterWidth();
                break;
            }
        }
    }

    private void computeTargetWidthAndHeight() {

        // In GRD products, all the bands will have the same width and height, so the width and height of the
        // product are equal to the width and height of the bands. E.g a GRD product will have these bands:
        // - Amplitude_VH
        // - Amplitude_VV
        // They will have the same width and height.
        //
        // In SLC products, each band belongs to a swath and bands belonging to the same swath will have the
        // same width and height. E.g. a SLC product will have these bands:
        //
        // IW1: w x h = 22553 x 14850
        // - i_IW1_VH
        // - q_IW1_VH
        // - i_IW1_VV
        // - q_IW1_VV
        //
        // IW2: w x h = 26326 x 15363
        // - i_IW2_VH
        // - q_IW2_VH
        // - i_IW2_VV
        // - q_IW2_VV
        //
        // IW3: w x h = 25321 x 15534
        // - i_IW3_VH
        // - q_IW3_VH
        // - i_IW3_VV
        // - q_IW3_VV
        //
        // We assume for such a SLC slice product, the scene raster width and height will be the maximum
        // among the swaths, so product scene raster width = 26326 and product scene raster height = 15534.
        //
        // Say 1st slice:
        //  IW1: w x h = 22553 x 14850
        //  IW2: w x h = 26326 x 15363
        //  IW3: w x h = 25321 x 15534
        //  product width = 26326 and height = 15534
        //
        // 2nd slice:
        //  IW1: w x h = 22571 x 14850
        //  IW2: w x h = 26351 x 15363
        //  IW3: w x h = 25350 x 15543
        // product width = 26351 and height = 15543
        //
        // Assemble the 2 slices:
        //  IW1: w x h = 22571 x 29700
        //  IW2: w x h = 26351 x 30726
        //  IW3: w x h = 25350 x 31077
        // product width = 26351 and height = 31077

        final String productType = sliceProducts[0].getProductType();

        if (productType.equals("GRD")) {

            for (Product srcProduct : sliceProducts) {

                if (targetWidth < srcProduct.getSceneRasterWidth())
                    targetWidth = srcProduct.getSceneRasterWidth();
                targetHeight += srcProduct.getSceneRasterHeight();

                final Map<String, int[]> tmp = new HashMap<>();
                tmp.put("", new int[] { srcProduct.getSceneRasterHeight(), srcProduct.getSceneRasterWidth() });
                sliceSwathImageDimMap.put(srcProduct, tmp);
            }

            swathAssembledImageDimMap.put("", new int[] { targetHeight, targetWidth });

        } else {

            final ArrayList<String> swaths = getSwaths(sliceProducts[0]);
            final Map<String, Integer> swathHeight = new HashMap<>();
            final Map<String, Integer> swathWidth = new HashMap<>();

            for (String swath : swaths) {
                swathHeight.put(swath, 0);
                swathWidth.put(swath, 0);
            }

            for (Product srcProduct : sliceProducts) {

                for (String swath : swaths) {

                    final int[] dim = new int[2];
                    getSwathDim(srcProduct, swath, dim);

                    if (swathWidth.get(swath) < dim[1]) {
                        swathWidth.replace(swath, dim[1]);
                    }
                    swathHeight.replace(swath, swathHeight.get(swath) + dim[0]);

                    if (sliceSwathImageDimMap.containsKey(srcProduct)) {
                        final Map<String, int[]> tmp = sliceSwathImageDimMap.get(srcProduct);
                        tmp.put(swath, dim);
                    } else {
                        final Map<String, int[]> tmp = new HashMap<>();
                        tmp.put(swath, dim);
                        sliceSwathImageDimMap.put(srcProduct, tmp);
                    }
                }
            }

            for (String swath : swaths) {
                swathAssembledImageDimMap.put(swath, new int[] { swathHeight.get(swath), swathWidth.get(swath) });
                if (targetWidth < swathWidth.get(swath))
                    targetWidth = swathWidth.get(swath);
                if (targetHeight < swathHeight.get(swath))
                    targetHeight = swathHeight.get(swath);
            }
        }
    }

    private void computeTargetBandWidthAndHeight(final String bandName, final Dimension dim)
            throws OperatorException {

        // See comments in computeTargetWidthAndHeight().
        // For band width, we take the max for that band among all slice products.

        for (Product srcProduct : sliceProducts) {
            final Band srcBand = srcProduct.getBand(bandName);
            if (srcBand == null) {
                throw new OperatorException(bandName + " not found in product " + srcProduct.getName());
            }

            dim.setSize(Math.max(dim.width, srcBand.getRasterWidth()), dim.height + srcBand.getRasterHeight());
        }
    }

    private void createTargetProduct() {
        computeTargetWidthAndHeight();

        final Product firstSliceProduct = sliceProducts[0];
        final Product lastSliceProduct = sliceProducts[sliceProducts.length - 1];
        final String lastSliceStopDateAndTime = lastSliceProduct.getName().substring(33, 48);
        final String newProductName = firstSliceProduct.getName().substring(0, 33) + lastSliceStopDateAndTime
                + firstSliceProduct.getName().substring(48);
        targetProduct = new Product(newProductName, firstSliceProduct.getProductType(), targetWidth, targetHeight);

        // We are creating each target band based on the source band in the first slice product only.
        final Band[] sourceBands = firstSliceProduct.getBands();
        for (Band srcBand : sourceBands) {
            boolean selectedPol = false;
            for (String pol : selectedPolarisations) {
                if (srcBand.getName().contains(pol))
                    selectedPol = true;
            }
            if (!selectedPol)
                continue;

            if (srcBand instanceof VirtualBand) {
                final VirtualBand sourceBand = (VirtualBand) srcBand;
                int destWidth = 0;
                int destHeight = 0;

                final Term term = createTerm(sourceBand.getExpression(), sliceProducts);
                final RasterDataSymbol[] refRasterDataSymbols = BandArithmetic.getRefRasterDataSymbols(term);
                for (RasterDataSymbol symbol : refRasterDataSymbols) {
                    String name = symbol.getName();
                    final Band trgBand = targetProduct.getBand(name);
                    if (trgBand != null) {
                        destWidth = trgBand.getRasterWidth();
                        destHeight = trgBand.getRasterHeight();
                        break;
                    }
                }

                final VirtualBand targetBand = new VirtualBand(sourceBand.getName(), sourceBand.getDataType(),
                        destWidth, destHeight, sourceBand.getExpression());

                ProductUtils.copyRasterDataNodeProperties(sourceBand, targetBand);
                targetProduct.addBand(targetBand);
            } else {
                final Dimension dim = new Dimension(0, 0);
                computeTargetBandWidthAndHeight(srcBand.getName(), dim);
                final Band newBand = new Band(srcBand.getName(), srcBand.getDataType(), dim.width, dim.height);
                ProductUtils.copyRasterDataNodeProperties(srcBand, newBand);

                targetProduct.addBand(newBand);
            }
        }

        ProductUtils.copyMetadata(firstSliceProduct, targetProduct);
        ProductUtils.copyFlagCodings(firstSliceProduct, targetProduct);
        ProductUtils.copyMasks(firstSliceProduct, targetProduct);
        ProductUtils.copyVectorData(firstSliceProduct, targetProduct);
        ProductUtils.copyIndexCodings(firstSliceProduct, targetProduct);
        targetProduct.setStartTime(firstSliceProduct.getStartTime());
        targetProduct.setEndTime(lastSliceProduct.getEndTime());
        targetProduct.setDescription(firstSliceProduct.getDescription());

        final String productType = absRoot.getAttributeString(AbstractMetadata.PRODUCT_TYPE);
        if (productType.equals("GRD")) {
            createTiePointGrids("");
        } else {
            final ArrayList<String> swaths = getSwaths(firstSliceProduct);
            for (String swath : swaths) {
                createTiePointGrids(swath);
            }
            createLatLonTiePointGridsForSLC();
        }

        addGeocoding();
    }

    private Term createTerm(final String expression, final Product[] availableProducts) {
        WritableNamespace namespace = BandArithmetic.createDefaultNamespace(availableProducts, 0);
        final Term term;
        try {
            Parser parser = new ParserImpl(namespace, false);
            term = parser.parse(expression);
        } catch (ParseException e) {
            throw new OperatorException("Could not parse expression: " + expression, e);
        }
        return term;
    }

    private MetadataElement[] getGeoGridForSwath(final Product product, final String sss) {

        // For GRD products, use "" for sss.

        final MetadataElement origProdRoot = AbstractMetadata.getOriginalProductMetadata(product);
        final MetadataElement annotationElem = origProdRoot.getElement("annotation");

        final MetadataElement[] images = annotationElem.getElements();

        MetadataElement imgElem = null;
        if (sss.equals("")) {
            // This is GRD product, same grid for all bands, so just take the 1st one
            imgElem = annotationElem.getElementAt(0);
        } else {
            for (MetadataElement e : images) {
                if (extractSwathIdentifier(e.getName()).equals(sss.toLowerCase())) {
                    imgElem = e;
                    break;
                }
            }
        }

        if (imgElem == null) {
            throw new OperatorException("Cannot locate geolocation grid in metadata for " + sss);
        }

        final MetadataElement productElem = imgElem.getElement("product");
        final MetadataElement geolocationGrid = productElem.getElement("geolocationGrid");
        final MetadataElement geolocationGridPointList = geolocationGrid.getElement("geolocationGridPointList");
        return geolocationGridPointList.getElements();
    }

    private void createTiePointGrids(final String swath) {

        //System.out.println("SliceAssemblyOp.createTiePointGrids: " + swath);

        // For GRD, use "" for swath. For SLC, it should be something like "IW1".

        // One geolocationGridPointList for one swath.

        // geolocationGridPointList has a count and a list of geolocationGridPoint(s).
        // These geolocationGridPoint(s) should form an MxN grid, M = number of rows and N = number of columns.
        // Each geolocationGridPoint contains line (i.e. row) and pixel (i.e. column).
        // The horizontal or vertical spacing may not be even.
        // But from line to line, the pixels are in the same location.
        // E.g A 4x6 grid for an image of width 24 and height 17:
        // 1st row: line = 0;   pixels =   0   5   10   15   20   23
        // 2nd row: line = 6;   pixels =    0   5   10   15   20   23
        // 3rd row: line = 12;   pixels =    0   5   10   15   20   23
        // 4th row: line = 16;  pixels =    0   5   10   15   20   23
        // According to Table 6-88 of Product Specs v2.9, each geolocationGridPoint is a point within the image.
        // So we cannot have a point with line 17 or pixels 24.
        //
        // We assume that from slice to slice, M may differ but N must be the same. However, the pixel spacing
        // from slice to slice may not be the same.
        // So the next slice to the example above can be a 5x6 grid for an image of width 30 and height 20:
        // 1st row: line = 0;   pixels =   0   6   12   18   24   29
        // 2nd row: line = 5;   pixels =    0   6   12   18   24   29
        // 3rd row: line = 10;   pixels =    0   6   12   18   24   29
        // 4th row: line = 15;  pixels =    0   6   12   18   24   29
        // 5th row: line = 19;  pixels =    0   6   12   18   24   29
        //
        // We concatenate the two geolocationGridPointList(s) together to form a 9x6 grid for an image of width 30 and
        // height 37:
        // 1st row: line = 0;   pixels =   0   5   10   15   20   23
        // 2nd row: line = 6;   pixels =    0   5   10   15   20   23
        // 3rd row: line = 12;   pixels =    0   5   10   15   20   23
        // 4th row: line = 16;  pixels =    0   5   10   15   20   23
        // 5th row: line = 17;   pixels =   0   6   12   18   24   29
        // 6th row: line = 22;   pixels =    0   6   12   18   24   29
        // 7th row: line = 27;   pixels =    0   6   12   18   24   29
        // 8th row: line = 32;  pixels =    0   6   12   18   24   29
        // 9th row: line = 36;  pixels =    0   6   12   18   24   29

        int geoGridLen = 0;
        final ArrayList<MetadataElement[]> geoGrids = new ArrayList<>();
        int i = 0;
        for (Product product : sliceProducts) {
            MetadataElement[] geoGrid = getGeoGridForSwath(product, swath);
            geoGridLen += geoGrid.length;
            geoGrids.add(i, geoGrid);
            i++;
        }

        //System.out.println("geoGridLen = " + geoGridLen);

        final double[] latList = new double[geoGridLen];
        final double[] lngList = new double[geoGridLen];
        final double[] incidenceAngleList = new double[geoGridLen];
        final double[] elevAngleList = new double[geoGridLen];
        final double[] rangeTimeList = new double[geoGridLen];
        final int[] x = new int[geoGridLen];
        final int[] y = new int[geoGridLen];

        final int[] gridWidths = new int[sliceProducts.length];
        final int[] gridHeights = new int[sliceProducts.length];

        for (int j = 0; j < sliceProducts.length; j++) {
            gridWidths[j] = 0;
            gridHeights[j] = 0;
        }

        int gridHeight = 0;

        i = 0;
        int ptsInPrvSlices = 0;
        int heightOffset = 0;
        for (int j = 0; j < sliceProducts.length; j++) {

            if (j > 0) {
                heightOffset += sliceSwathImageDimMap.get(sliceProducts[j - 1]).get(swath)[0];
            }

            final MetadataElement[] geoGrid = geoGrids.get(j);

            for (MetadataElement ggPoint : geoGrid) {
                latList[i] = ggPoint.getAttributeDouble("latitude", 0);
                lngList[i] = ggPoint.getAttributeDouble("longitude", 0);
                incidenceAngleList[i] = ggPoint.getAttributeDouble("incidenceAngle", 0);
                elevAngleList[i] = ggPoint.getAttributeDouble("elevationAngle", 0);
                rangeTimeList[i] = ggPoint.getAttributeDouble("slantRangeTime", 0) * Constants.oneBillion; // s to ns

                x[i] = (int) ggPoint.getAttributeDouble("pixel", 0);
                if (x[i] == 0) {
                    // This means we are at the start of a new line
                    // gridWidths[j] will be updated to 0 at the 1st line.
                    // It will be updated to the correct value at the 2nd line.
                    if (gridWidths[j] == 0)
                        gridWidths[j] = i - ptsInPrvSlices;
                    ++gridHeights[j];
                }

                y[i] = (int) ggPoint.getAttributeDouble("line", 0) + heightOffset;

                ++i;
            }

            ptsInPrvSlices = i;

            gridHeight += gridHeights[j];
        }

        final int gridWidth = gridWidths[0];

        // We assume here that all the slice products will have the same width in the geolocation grid.
        // That is the same number of points in each line.
        for (int w : gridWidths) {
            if (w != gridWidth) {
                throw new OperatorException("geolocation grids have different widths among slice products");
            }
        }

        //System.out.println("gridWidth = " + gridWidth + " gridHeight = " + gridHeight);

        final int newGridWidth = gridWidth;
        final int newGridHeight = gridHeight;
        if (geoGridLen != (newGridWidth * newGridHeight)) {
            throw new OperatorException("wrong number of geolocation grid points");
        }
        final float[] newLatList = new float[newGridWidth * newGridHeight];
        final float[] newLonList = new float[newGridWidth * newGridHeight];
        final float[] newIncList = new float[newGridWidth * newGridHeight];
        final float[] newElevList = new float[newGridWidth * newGridHeight];
        final float[] newslrtList = new float[newGridWidth * newGridHeight];

        final int[] dim = swathAssembledImageDimMap.get(swath);
        final int sceneRasterWidth = dim[1];
        final int sceneRasterHeight = dim[0];

        //System.out.println("swath = " + swath + " width = " + sceneRasterWidth + " height = " + sceneRasterHeight);

        final double subSamplingX = (double) sceneRasterWidth / (newGridWidth - 1);
        final double subSamplingY = (double) sceneRasterHeight / (newGridHeight - 1);

        getListInEvenlySpacedGrid(sceneRasterWidth, sceneRasterHeight, gridWidth, gridHeight, x, y, latList,
                newGridWidth, newGridHeight, subSamplingX, subSamplingY, newLatList);

        getListInEvenlySpacedGrid(sceneRasterWidth, sceneRasterHeight, gridWidth, gridHeight, x, y, lngList,
                newGridWidth, newGridHeight, subSamplingX, subSamplingY, newLonList);

        getListInEvenlySpacedGrid(sceneRasterWidth, sceneRasterHeight, gridWidth, gridHeight, x, y,
                incidenceAngleList, newGridWidth, newGridHeight, subSamplingX, subSamplingY, newIncList);

        getListInEvenlySpacedGrid(sceneRasterWidth, sceneRasterHeight, gridWidth, gridHeight, x, y, elevAngleList,
                newGridWidth, newGridHeight, subSamplingX, subSamplingY, newElevList);

        getListInEvenlySpacedGrid(sceneRasterWidth, sceneRasterHeight, gridWidth, gridHeight, x, y, rangeTimeList,
                newGridWidth, newGridHeight, subSamplingX, subSamplingY, newslrtList);

        final String prefix = swath.equals("") ? swath : swath + "_";

        final TiePointGrid latGrid = new TiePointGrid(prefix + OperatorUtils.TPG_LATITUDE, newGridWidth,
                newGridHeight, 0.5f, 0.5f, subSamplingX, subSamplingY, newLatList);
        latGrid.setUnit(Unit.DEGREES);
        targetProduct.addTiePointGrid(latGrid);

        final TiePointGrid lonGrid = new TiePointGrid(prefix + OperatorUtils.TPG_LONGITUDE, newGridWidth,
                newGridHeight, 0.5f, 0.5f, subSamplingX, subSamplingY, newLonList, TiePointGrid.DISCONT_AT_180);
        lonGrid.setUnit(Unit.DEGREES);
        targetProduct.addTiePointGrid(lonGrid);

        final TiePointGrid incidentAngleGrid = new TiePointGrid(prefix + OperatorUtils.TPG_INCIDENT_ANGLE,
                newGridWidth, newGridHeight, 0.5f, 0.5f, subSamplingX, subSamplingY, newIncList);
        incidentAngleGrid.setUnit(Unit.DEGREES);
        targetProduct.addTiePointGrid(incidentAngleGrid);

        final TiePointGrid elevAngleGrid = new TiePointGrid(prefix + OperatorUtils.TPG_ELEVATION_ANGLE,
                newGridWidth, newGridHeight, 0.5f, 0.5f, subSamplingX, subSamplingY, newElevList);
        elevAngleGrid.setUnit(Unit.DEGREES);
        targetProduct.addTiePointGrid(elevAngleGrid);

        final TiePointGrid slantRangeGrid = new TiePointGrid(prefix + OperatorUtils.TPG_SLANT_RANGE_TIME,
                newGridWidth, newGridHeight, 0.5f, 0.5f, subSamplingX, subSamplingY, newslrtList);
        slantRangeGrid.setUnit(Unit.NANOSECONDS);
        targetProduct.addTiePointGrid(slantRangeGrid);

        if (!swath.equals("")) {
            // This is SLC
            final String swathNum = swath.substring(swath.length() - 1, swath.length());
            if (swathNum.equals("1") || swathNum.equals(Integer.toString(swathAssembledImageDimMap.size()))) {
                final TiePointGeoCoding tpGeoCoding = new TiePointGeoCoding(latGrid, lonGrid, Datum.WGS_84);
                swathGeocodingMap.put(swath, tpGeoCoding);
            }
        }

        //System.out.println("SliceAssemblyOp.createTiePointGrids: DONE " + swath);
    }

    private void createLatLonTiePointGridsForSLC() {

        final MetadataElement absRoot = AbstractMetadata.getAbstractedMetadata(sliceProducts[0]);
        final String acquisitionMode = absRoot.getAttributeString(AbstractMetadata.ACQUISITION_MODE);

        final String firstSwath = acquisitionMode + "1";
        final String lastSwath = acquisitionMode + Integer.toString(swathAssembledImageDimMap.size());

        final GeoCoding firstSWBandGeoCoding = swathGeocodingMap.get(firstSwath);
        final int firstSWBandHeight = swathAssembledImageDimMap.get(firstSwath)[0];

        final GeoCoding lastSWBandGeoCoding = swathGeocodingMap.get(lastSwath);
        final int lastSWBandWidth = swathAssembledImageDimMap.get(lastSwath)[1];
        final int lastSWBandHeight = swathAssembledImageDimMap.get(lastSwath)[0];

        final PixelPos ulPix = new PixelPos(0, 0);
        final PixelPos llPix = new PixelPos(0, firstSWBandHeight - 1);
        final GeoPos ulGeo = new GeoPos();
        final GeoPos llGeo = new GeoPos();
        firstSWBandGeoCoding.getGeoPos(ulPix, ulGeo);
        firstSWBandGeoCoding.getGeoPos(llPix, llGeo);

        final PixelPos urPix = new PixelPos(lastSWBandWidth - 1, 0);
        final PixelPos lrPix = new PixelPos(lastSWBandWidth - 1, lastSWBandHeight - 1);
        final GeoPos urGeo = new GeoPos();
        final GeoPos lrGeo = new GeoPos();
        lastSWBandGeoCoding.getGeoPos(urPix, urGeo);
        lastSWBandGeoCoding.getGeoPos(lrPix, lrGeo);

        final float[] latCorners = { (float) ulGeo.getLat(), (float) urGeo.getLat(), (float) llGeo.getLat(),
                (float) lrGeo.getLat() };
        final float[] lonCorners = { (float) ulGeo.getLon(), (float) urGeo.getLon(), (float) llGeo.getLon(),
                (float) lrGeo.getLon() };

        ReaderUtils.addGeoCoding(targetProduct, latCorners, lonCorners);

        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.first_near_lat, ulGeo.getLat());
        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.first_near_long, ulGeo.getLon());
        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.first_far_lat, urGeo.getLat());
        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.first_far_long, urGeo.getLon());

        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.last_near_lat, llGeo.getLat());
        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.last_near_long, llGeo.getLon());
        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.last_far_lat, lrGeo.getLat());
        AbstractMetadata.setAttribute(absRoot, AbstractMetadata.last_far_long, lrGeo.getLon());
    }

    private void addGeocoding() {
        final TiePointGrid latGrid = targetProduct.getTiePointGrid(OperatorUtils.TPG_LATITUDE);
        final TiePointGrid lonGrid = targetProduct.getTiePointGrid(OperatorUtils.TPG_LONGITUDE);

        final TiePointGeoCoding tpGeoCoding = new TiePointGeoCoding(latGrid, lonGrid, Datum.WGS_84);
        targetProduct.setGeoCoding(tpGeoCoding);
    }

    private String extractImageNumber(final String filename) {

        final int dotIdx = filename.indexOf('.');
        return filename.substring(dotIdx - 3, dotIdx);
    }

    private ProductData getStopTime(final Product product, final String imageNum) {

        //System.out.println("getStopTime for " + product.getName() + " imageNum = " + imageNum);

        final MetadataElement origProdRoot = AbstractMetadata.getOriginalProductMetadata(product);
        final MetadataElement calibration = origProdRoot.getElement("calibration");
        final MetadataElement[] calibrationElems = calibration.getElements();

        ProductData data = null;

        for (MetadataElement e : calibrationElems) {

            //System.out.println("getStopTime: " + e.getName());

            if (extractImageNumber(e.getName()).equals(imageNum)) {

                final MetadataElement calib = e.getElement("calibration");
                final MetadataElement adsHeader = calib.getElement("adsHeader");
                final MetadataAttribute stopTime = adsHeader.getAttribute("stopTime");
                //System.out.println("getStopTime: " + stopTime.getData().toString());

                data = stopTime.getData();
            }

        }

        return data;
    }

    MetadataElement getCalibrationOrNoiseVectorList(final Product product, final String imageNum,
            final String dataName) {

        final MetadataElement origProdRoot = AbstractMetadata.getOriginalProductMetadata(product);
        final MetadataElement data = origProdRoot.getElement(dataName);
        final MetadataElement[] elems = data.getElements();

        MetadataElement vectorList = null;

        for (MetadataElement e : elems) {

            if (extractImageNumber(e.getName()).equals(imageNum)) {

                final MetadataElement dat = e.getElement(dataName);
                vectorList = dat.getElement(dataName + "VectorList");
            }

        }

        return vectorList;
    }

    private int getCalibrationOrNoisePixelCount(final Product product, final String imageNum,
            final int[] pixelSpacing, final String dataName) {

        final MetadataElement origProdRoot = AbstractMetadata.getOriginalProductMetadata(product);
        final MetadataElement calibration = origProdRoot.getElement(dataName);
        final MetadataElement[] calibrationElems = calibration.getElements();

        int pixelCount = 0;

        for (MetadataElement e : calibrationElems) {
            //System.out.println("getCalibrationOrNoisePixelCount: " + e.getName());
            if (extractImageNumber(e.getName()).equals(imageNum)) {
                final MetadataElement calib = e.getElement(dataName);
                final MetadataElement calibVectorList = calib.getElement(dataName + "VectorList");
                final MetadataElement firstCalibVector = calibVectorList.getElementAt(0);
                final MetadataElement pixel = firstCalibVector.getElement("pixel");

                final MetadataAttribute count = pixel.getAttribute("count");
                //System.out.println("getCalibrationOrNoisePixelCount: " + count.getData().toString());
                pixelCount = Integer.parseInt(count.getData().getElemString());

                final MetadataAttribute pixels = pixel.getAttribute("pixel");
                final String pixelsStr = pixels.getData().getElemString();
                final String[] pixelsArrayOfStr = pixelsStr.split(" ");
                final int pixel0 = Integer.parseInt(pixelsArrayOfStr[0]);
                final int pixel1 = Integer.parseInt(pixelsArrayOfStr[1]);
                //System.out.println("pixel0 = " + pixel0 + " pixel1 = " + pixel1);
                pixelSpacing[0] = pixel1 - pixel0;
            }
        }

        return pixelCount;
    }

    private void concatenateVectors(final MetadataElement targetVectorList, final MetadataElement sliceVectorList,
            final int startVectorIdx, final int lineOffset) {

        int idx = Integer.parseInt(targetVectorList.getAttribute("count").getData().getElemString());
        final int numSliceLines = Integer.parseInt(sliceVectorList.getAttribute("count").getData().getElemString());

        for (int i = startVectorIdx; i < numSliceLines; i++) {

            MetadataElement v = sliceVectorList.getElementAt(i);
            MetadataElement newV = v.createDeepClone();
            final int newLine = Integer.parseInt(v.getAttributeString("line")) + lineOffset;
            newV.setAttributeString("line", Integer.toString(newLine));
            targetVectorList.addElementAt(newV, idx);
            idx++;
        }

        targetVectorList.setAttributeString("count", Integer.toString(idx));
    }

    private void updateCalibrationOrNoise(final String dataName) {

        // dataName should be "calibration" or "noise"

        final Product lastSliceProduct = sliceProducts[sliceProducts.length - 1];

        // The calibration or noise data in metadata in targetProduct at this point is copied from the 1st slice.
        // So we need to concatenate the vectors from the slices to target.

        // TODO: We should make the short vectors longer so that all vectors have "same" columns.
        // TODO: E.g., a vector from top slice (width = 20055) has pixels:
        // TODO: 0 40 80 ... 20040 20056
        // TODO: a vector from the bottom slice (width = 20086) has pixels:
        // TODO: 0 40 80 ... 20040 20080 20086
        // TODO: Assembled product has width 20086 and we need to use extrapolation to extend the vectors from the top
        // TODO: slice to have pixels:
        // TODO: 0 40 80 ... 20040 20080 20086

        final MetadataElement targetOrigProdRoot = AbstractMetadata.getOriginalProductMetadata(targetProduct);
        final MetadataElement targetData = targetOrigProdRoot.getElement(dataName);
        final MetadataElement[] targetDataElems = targetData.getElements();

        // loop through each s1...-nnn.xml where nnn is the image number
        for (MetadataElement target : targetDataElems) {

            boolean isSelected = false;
            for (String pol : selectedPolarisations) {
                if (target.getName().toUpperCase().contains(pol)) {
                    isSelected = true;
                    break;
                }
            }

            if (!isSelected) {
                //System.out.println("remove " + dataName + " for " + target.getName());
                targetData.removeElement(target);
                continue;
            }

            //System.out.println("update " + dataName + " for " + target.getName());

            final String imageNum = extractImageNumber(target.getName());
            final MetadataElement targetDat = target.getElement(dataName);

            // Update stopTime in adsHeader
            final MetadataElement targetADSHeader = targetDat.getElement("adsHeader");
            final ProductData lastSliceStopTime = getStopTime(lastSliceProduct, imageNum);
            AbstractMetadata.setAttribute(targetADSHeader, "stopTime", lastSliceStopTime.getElemString());

            // Update (calibration|noise)VectorList

            final MetadataElement targetVectorList = targetDat.getElement(dataName + "VectorList");

            int numLines = Integer.parseInt(targetVectorList.getAttribute("count").getData().getElemString());
            final int[] pixelSpacing = new int[1];
            int numPixels = getCalibrationOrNoisePixelCount(targetProduct, imageNum, pixelSpacing, dataName);
            int height = sliceProducts[0].getSceneRasterHeight();
            //System.out.println("initial numLines = " + numLines + " numPixels = " + numPixels + " height = " + height);

            // Loop through 2nd to last slice products in order and concatenate the (calibration|noise) vectors from each
            // slice to the bottom of target
            for (int i = 1; i < sliceProducts.length; i++) {

                final Product sliceProduct = sliceProducts[i];
                final int[] slicePixelSpacing = new int[1];
                final int sliceNumPixels = getCalibrationOrNoisePixelCount(sliceProduct, imageNum,
                        slicePixelSpacing, dataName);

                if (pixelSpacing[0] != slicePixelSpacing[0]) {
                    throw new OperatorException("slice products have different pixel spacing in " + dataName
                            + " vectors: " + i + " " + pixelSpacing[0] + " " + slicePixelSpacing[0]);
                }

                final MetadataElement targetLastVector = targetVectorList
                        .getElementAt(targetVectorList.getNumElements() - 1);
                int targetLastVectorLine = Integer.parseInt(targetLastVector.getAttributeString("line"));
                //System.out.println("targetLastVectorLine = " + targetLastVectorLine + " sliceNumPixels = " + sliceNumPixels);

                final MetadataElement sliceVectorList = getCalibrationOrNoiseVectorList(sliceProduct, imageNum,
                        dataName);
                final int sliceNumLines = Integer
                        .parseInt(sliceVectorList.getAttribute("count").getData().getElemString());

                final int topSliceWidth = sliceProducts[i - 1].getSceneRasterWidth();
                final int bottomSliceWidth = sliceProducts[i].getSceneRasterWidth();

                int numLinesRemoved = 0;

                if (targetLastVectorLine == height - 1) {

                    concatenateVectors(targetVectorList, sliceVectorList, 0, height);

                } else if (numPixels <= sliceNumPixels && topSliceWidth <= bottomSliceWidth) {

                    // Remove excess calibration vectors from target and keep all calibration vectors from bottom
                    // slice

                    int j;
                    for (j = numLines - 1; j >= 0; j--) {
                        MetadataElement targetCalibVector = targetVectorList.getElementAt(j);
                        final int targetCalibVectorLine = Integer
                                .parseInt(targetCalibVector.getAttributeString("line"));
                        if (targetCalibVectorLine >= height) {
                            targetVectorList.removeElement(targetCalibVector);
                        } else {
                            break;
                        }
                    }

                    numLinesRemoved = numLines - j - 1;
                    // Must update targetCalibVectorList count before calling concatenateCalibrationVectors()
                    targetVectorList.setAttributeString("count", Integer.toString(numLines - numLinesRemoved));
                    concatenateVectors(targetVectorList, sliceVectorList, 0, height);

                } else {

                    int j;

                    // Remove excess calibration vectors at the bottom in target metadata.
                    // "numLines" is thee current number of calibration vectors in the target metadata (with i slices assembled).
                    // "height" is the height of the image after assembling i slices.
                    // Say, height is 975 and there are calibration vectors at line 950, 1000, 1050, 1100. We can remove
                    // those for lines 1050 and 1100. There are slice products with such excess calibration vectors.
                    // They need to be removed before we concatenate the calibration vectors from the next slice.
                    for (j = numLines - 1; j >= 0; j--) {
                        MetadataElement targetCalibVector = targetVectorList.getElementAt(j);
                        final int targetCalibVectorLine = Integer
                                .parseInt(targetCalibVector.getAttributeString("line"));
                        if (targetCalibVectorLine < height - 1) {
                            // We need one calibration vector whose line is >= height-1 (height-1 is last line of image)
                            // so the j+1 is the last calibration vector we need to keep
                            break;
                        }
                        targetLastVectorLine = targetCalibVectorLine;
                    }
                    // Want to keep j+1 as last calibration vector, start removing at j+2.
                    for (int k = j + 2; k < numLines; k++) {
                        MetadataElement targetCalibVector = targetVectorList.getElementAt(k);
                        targetVectorList.removeElement(targetCalibVector);
                    }
                    targetVectorList.setAttributeString("count", Integer.toString(j + 2));
                    numLinesRemoved = numLines - (j + 2);

                    // Since the slice we are concatenating to target is smaller in width, we want to skip the
                    // starting calibration vectors whose lines are <= the line of the last target calibration vector.
                    // "sliceNumLines" is the number of calibration vectors in the slice.
                    // Note that the line of each slice calibration vector has zero offset, so we have to add "height"
                    // to it before comparing with target.
                    for (j = 0; j < sliceNumLines; j++) {
                        final MetadataElement sliceCalibVector = sliceVectorList.getElementAt(j);
                        final int sliceCalibVectorLine = Integer
                                .parseInt(sliceCalibVector.getAttributeString("line"));
                        if (sliceCalibVectorLine + height > targetLastVectorLine) {
                            // j is the first one we want to keep
                            break;
                        }
                    }
                    numLinesRemoved += j;

                    concatenateVectors(targetVectorList, sliceVectorList, j, height);
                }

                numLines += (sliceNumLines - numLinesRemoved);
                targetVectorList.setAttributeString("count", Integer.toString(numLines));
                numPixels = sliceNumPixels;
                height += sliceProduct.getSceneRasterHeight();
            }
        }
    }

    private String getProductLastLineUtcTime(final Product product, final String imageNum) {

        final MetadataElement targetOrigProdRoot = AbstractMetadata.getOriginalProductMetadata(product);
        final MetadataElement targetData = targetOrigProdRoot.getElement("annotation");

        final MetadataElement[] targetDataElems = targetData.getElements();

        for (MetadataElement target : targetDataElems) {

            if (extractImageNumber(target.getName()).equals(imageNum)) {
                final MetadataElement productElem = target.getElement("product");
                final MetadataElement imageAnnotationElem = productElem.getElement("imageAnnotation");
                final MetadataElement imageInformationElem = imageAnnotationElem.getElement("imageInformation");
                return imageInformationElem.getAttributeString("productLastLineUtcTime");
            }
        }

        return "";
    }

    private void updateImageInformation() {

        final MetadataElement targetOrigProdRoot = AbstractMetadata.getOriginalProductMetadata(targetProduct);
        final MetadataElement targetData = targetOrigProdRoot.getElement("annotation");
        final MetadataElement[] targetDataElems = targetData.getElements();

        // loop through each s1...-nnn.xml where nnn is the image number
        for (MetadataElement target : targetDataElems) {

            boolean isSelected = false;
            for (String pol : selectedPolarisations) {
                if (target.getName().toUpperCase().contains(pol)) {
                    isSelected = true;
                    break;
                }
            }

            if (!isSelected) {
                //System.out.println("remove " + dataName + " for " + target.getName());
                targetData.removeElement(target);
                continue;
            }

            final MetadataElement productElem = target.getElement("product");
            final MetadataElement imageAnnotationElem = productElem.getElement("imageAnnotation");
            final MetadataElement imageInformationElem = imageAnnotationElem.getElement("imageInformation");

            imageInformationElem.setAttributeString("productLastLineUtcTime", getProductLastLineUtcTime(
                    sliceProducts[sliceProducts.length - 1], extractImageNumber(target.getName())));
            imageInformationElem.setAttributeString("numberOfSamples",
                    Integer.toString(targetProduct.getSceneRasterWidth()));
            imageInformationElem.setAttributeString("numberOfLines",
                    Integer.toString(targetProduct.getSceneRasterHeight()));
        }
    }

    private void updateTargetProductMetadata() throws Exception {

        // All the metadata has been copied from the 1st slice product to the assembled target product.
        // Now we want to update the metadata that should not be "included", but "merged" or concatenated".

        final MetadataElement absTgt = AbstractMetadata.getAbstractedMetadata(targetProduct);
        final Product firstSliceProduct = sliceProducts[0];
        final Product lastSliceProduct = sliceProducts[sliceProducts.length - 1];
        final MetadataElement absFirst = AbstractMetadata.getAbstractedMetadata(firstSliceProduct);
        final MetadataElement absLast = AbstractMetadata.getAbstractedMetadata(lastSliceProduct);

        AbstractMetadata.setAttribute(absTgt, AbstractMetadata.first_line_time,
                absFirst.getAttributeUTC(AbstractMetadata.first_line_time));
        AbstractMetadata.setAttribute(absTgt, AbstractMetadata.last_line_time,
                absLast.getAttributeUTC(AbstractMetadata.last_line_time));

        AbstractMetadata.setAttribute(absTgt, AbstractMetadata.num_output_lines, targetHeight);
        AbstractMetadata.setAttribute(absTgt, AbstractMetadata.num_samples_per_line, targetWidth);

        for (Band band : targetProduct.getBands()) {
            MetadataElement bandMeta = AbstractMetadata.getBandAbsMetadata(absTgt, band);

            AbstractMetadata.setAttribute(bandMeta, AbstractMetadata.first_line_time,
                    absFirst.getAttributeUTC(AbstractMetadata.first_line_time));
            AbstractMetadata.setAttribute(bandMeta, AbstractMetadata.last_line_time,
                    absLast.getAttributeUTC(AbstractMetadata.last_line_time));

            AbstractMetadata.setAttribute(absTgt, AbstractMetadata.num_output_lines, band.getRasterHeight());
            AbstractMetadata.setAttribute(absTgt, AbstractMetadata.num_samples_per_line, band.getRasterWidth());
        }

        final List<OrbitStateVector> orbVectorList = new ArrayList<>();
        final List<AbstractMetadata.SRGRCoefficientList> srgrList = new ArrayList<>();
        final List<AbstractMetadata.DopplerCentroidCoefficientList> dopList = new ArrayList<>();
        for (Product srcProduct : sliceProducts) {
            final MetadataElement absSrc = AbstractMetadata.getAbstractedMetadata(srcProduct);

            // update orbit state vectors
            final OrbitStateVector[] orbs = AbstractMetadata.getOrbitStateVectors(absSrc);
            orbVectorList.addAll(Arrays.asList(orbs));

            // update srgr coeffs
            final AbstractMetadata.SRGRCoefficientList[] srgr = AbstractMetadata.getSRGRCoefficients(absSrc);
            srgrList.addAll(Arrays.asList(srgr));

            // update Doppler centroid coeffs
            final AbstractMetadata.DopplerCentroidCoefficientList[] dop = AbstractMetadata
                    .getDopplerCentroidCoefficients(absSrc);
            dopList.addAll(Arrays.asList(dop));
        }

        AbstractMetadata.setOrbitStateVectors(absTgt,
                orbVectorList.toArray(new OrbitStateVector[orbVectorList.size()]));
        AbstractMetadata.setSRGRCoefficients(absTgt,
                srgrList.toArray(new AbstractMetadata.SRGRCoefficientList[srgrList.size()]));
        AbstractMetadata.setDopplerCentroidCoefficients(absTgt,
                dopList.toArray(new AbstractMetadata.DopplerCentroidCoefficientList[dopList.size()]));

        updateCalibrationOrNoise("calibration");
        updateCalibrationOrNoise("noise");

        updateImageInformation();

        //System.out.println("DONE updateTargetProductMetadata");
    }

    private void determineBandStartEndTimes() {
        for (Band targetBand : targetProduct.getBands()) {
            final List<BandLines> bandLineList = new ArrayList<>(sliceProducts.length);
            int height = 0;
            for (Product srcProduct : sliceProducts) {
                final Band srcBand = srcProduct.getBand(targetBand.getName());
                int start = height;
                height += srcBand.getRasterHeight();
                int end = height;
                bandLineList.add(new BandLines(srcBand, start, end));
            }
            final BandLines[] lines = bandLineList.toArray(new BandLines[bandLineList.size()]);
            bandLineMap.put(targetBand, lines);
        }
    }

    /**
     * Called by the framework in order to compute a tile for the given target band.
     * <p>The default implementation throws a runtime exception with the message "not implemented".</p>
     *
     * @param targetBand The target band.
     * @param targetTile The current tile associated with the target band to be computed.
     * @param pm         A progress monitor which should be used to determine computation cancellation requests.
     * @throws org.esa.beam.framework.gpf.OperatorException If an error occurs during computation of the target raster.
     */
    public void computeTile(Band targetBand, Tile targetTile, ProgressMonitor pm) throws OperatorException {

        try {
            final Rectangle targetTileRectangle = targetTile.getRectangle();
            final int tx0 = targetTileRectangle.x;
            final int ty0 = targetTileRectangle.y;
            final int maxY = ty0 + targetTileRectangle.height;
            final int maxX = tx0 + targetTileRectangle.width;

            final BandLines[] lines = bandLineMap.get(targetBand);
            final ProductData trgData = targetTile.getDataBuffer();

            final TileIndex trgIndex = new TileIndex(targetTile);
            final Rectangle srcRect = new Rectangle();

            //System.out.println("Do band = " + targetBand.getName() + ": tx0 = " + tx0 + " ty0 = " + ty0 + " maxX = " + maxX + " maxY = " + maxY);

            BandLines line = lines[0];
            for (int y = ty0; y < maxY; ++y) {

                boolean validLine = y >= line.start && y < line.end;
                if (!validLine) {
                    for (BandLines l : lines) {
                        if (y >= l.start && y < l.end) {
                            line = l;
                            validLine = true;
                            break;
                        }
                    }
                    if (!validLine) {
                        // should never get here
                        throw new OperatorException("line " + y + " not found in slice products");
                    }
                }

                final int yy = y - line.start;
                srcRect.setBounds(targetTileRectangle.x, yy, targetTileRectangle.width, 1);
                final Tile sourceRaster = getSourceTile(line.band, srcRect);
                final ProductData srcData = sourceRaster.getDataBuffer();
                final TileIndex srcIndex = new TileIndex(sourceRaster);
                trgIndex.calculateStride(y);
                srcIndex.calculateStride(yy);

                for (int x = tx0; x < maxX; ++x) {
                    trgData.setElemDoubleAt(trgIndex.getIndex(x), srcData.getElemDoubleAt(srcIndex.getIndex(x)));
                }
            }

            //System.out.println("DONE band = " + targetBand.getName() + ": tx0 = " + tx0 + " ty0 = " + ty0 + " maxX = " + maxX + " maxY = " + maxY);
        } catch (Throwable e) {
            throw new OperatorException(e.getMessage());
        }
    }

    private static class BandLines {
        final int start;
        final int end;
        final Band band;

        BandLines(final Band band, final int s, final int e) {
            this.band = band;
            this.start = s;
            this.end = e;
        }
    }

    /**
     * The SPI is used to register this operator in the graph processing framework
     * via the SPI configuration file
     * {@code META-INF/services/org.esa.beam.framework.gpf.OperatorSpi}.
     * This class may also serve as a factory for new operator instances.
     *
     * @see org.esa.beam.framework.gpf.OperatorSpi#createOperator()
     * @see org.esa.beam.framework.gpf.OperatorSpi#createOperator(java.util.Map, java.util.Map)
     */
    public static class Spi extends OperatorSpi {
        public Spi() {
            super(SliceAssemblyOp.class);
        }
    }
}