/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2005-2017 European Southern Observatory
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*----------------------------------------------------------------------------*
 *                              Includes                                      *
 *----------------------------------------------------------------------------*/
#include <cpl.h>
#include <math.h>
#include <string.h>
#ifndef _OPENMP
#define omp_get_max_threads() 1
#else
#include <omp.h>
#endif

#include "muse_resampling.h"
#include "muse_instrument.h"

#include "muse_astro.h"
#include "muse_cplwrappers.h"
#include "muse_dar.h"
#include "muse_dfs.h"
#include "muse_flux.h"
#include "muse_pfits.h"
#include "muse_pixgrid.h"
#include "muse_pixtable.h"
#include "muse_quadrants.h"
#include "muse_quality.h"
#include "muse_utils.h"
#include "muse_wcs.h"
#include "muse_data_format_z.h"

/*----------------------------------------------------------------------------*/
/**
 * @defgroup muse_resampling   Resampling
 */
/*----------------------------------------------------------------------------*/

/**@{*/

/*----------------------------------------------------------------------------*
 *                             Resampling, collapsing, and saving in 3D       *
 *----------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------*/
/**
  @brief   Create the resampling parameters structure.
  @param   aMethod   the resampling type/method to set
  @return  Pointer to the newly created structure or NULL on error.

  Passing MUSE_RESAMPLE_NONE is allowed; it signifies to create an empty
  datacube but cannot be used to resample to Euro3D.

  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL, an illegal method was given}
  @error{wcs component is left NULL\, so it triggers valid defaults later,
         aWCS is NULL}
*/
/*---------------------------------------------------------------------------*/
muse_resampling_params *
muse_resampling_params_new(muse_resampling_type aMethod)
{
  cpl_ensure(aMethod <= MUSE_RESAMPLE_NONE, CPL_ERROR_ILLEGAL_INPUT, NULL);
  muse_resampling_params *params = cpl_calloc(1, sizeof(muse_resampling_params));
  params->method = aMethod;
  /* leave crtype zero == MUSE_RESAMPLING_CRSTATS_IRAF by default */
  /* leave crsigma zero, to not do CR rejection by default */
  params->ld = 1;
  params->pfx = 0.6;
  params->pfy = 0.6;
  params->pfl = 0.6;
  params->rc = 1.25;
  /* dx, dy, and dlambda, and wcs stay zero to trigger defaults */
  /* leave tlambda zero == MUSE_RESAMPLING_DISP_AWAV by default */
  return params;
} /* muse_resampling_params_new() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Set an output WCS (and wavelength scale) in the resampling parameters.
  @param   aParams   the resampling parameters
  @param   aWCS      propertylist with a WCS
  @return  CPL_ERROR_NONE on success, another CPL error code on failure.

  Passing aWCS as NULL is allowed, it signifies to free and NULL out an existing
  WCS, the tlambda component is then set to MUSE_RESAMPLING_DISP_AWAV.
  Otherwise, tlambda is set to MUSE_RESAMPLING_DISP_AWAV (as default) or
  another valid muse_resampling_dispersion_type, and wcs is filled using
  cpl_wcs_new_from_propertylist().

  @error{return CPL_ERROR_NULL_INPUT, aParams is NULL}
  @error{wcs component is left NULL\, so it triggers valid defaults later,
         aWCS is NULL}
  @error{wcs component is set to NULL\, return code is propagated,
         cpl_wcs_new_from_propertylist() fails}
*/
/*---------------------------------------------------------------------------*/
cpl_error_code
muse_resampling_params_set_wcs(muse_resampling_params *aParams,
                               const cpl_propertylist *aWCS)
{
  cpl_ensure_code(aParams, CPL_ERROR_NULL_INPUT);
  if (!aWCS) {
    aParams->tlambda = MUSE_RESAMPLING_DISP_AWAV;
    cpl_wcs_delete(aParams->wcs);
    aParams->wcs = NULL;
    return CPL_ERROR_NONE;
  }
  /* first make sure it wasn't set to rubbish elsewhere */
  aParams->tlambda = MUSE_RESAMPLING_DISP_AWAV;
  if (cpl_propertylist_has(aWCS, "CTYPE3")) {
    if (!strncmp(muse_pfits_get_ctype(aWCS, 3), "AWAV-LOG", 9)) {
      aParams->tlambda = MUSE_RESAMPLING_DISP_AWAV_LOG;
    } else if (!strncmp(muse_pfits_get_ctype(aWCS, 3), "WAVE", 5)) {
      aParams->tlambda = MUSE_RESAMPLING_DISP_WAVE;
    } else if (!strncmp(muse_pfits_get_ctype(aWCS, 3), "WAVE-LOG", 9)) {
      aParams->tlambda = MUSE_RESAMPLING_DISP_WAVE_LOG;
    } else {
      /* MUSE_RESAMPLING_DISP_AWAV, do nothing */
    }
  } /* if CTYPE3 given */
  cpl_errorstate state = cpl_errorstate_get();
  cpl_error_code rc = CPL_ERROR_NONE;
  aParams->wcs = cpl_wcs_new_from_propertylist(aWCS);
  if (!cpl_errorstate_is_equal(state)) {
    cpl_wcs_delete(aParams->wcs);
    aParams->wcs = NULL;
    rc = cpl_error_get_code();
  }
#if 0
  printf("CDi_j matrix:\n");
  cpl_matrix_dump(cpl_wcs_get_cd(aParams->wcs), stdout);
  fflush(stdout);
#endif
  return rc;
} /* muse_resampling_params_set_wcs() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Delete a resampling parameters structure.
  @param   aParams   Pointer to the structure.

  This only calls cpl_free, but it's nice to have a function that reverses
  muse_resampling_params_new().
*/
/*---------------------------------------------------------------------------*/
void
muse_resampling_params_delete(muse_resampling_params *aParams)
{
  if (!aParams) {
    return;
  }
  cpl_wcs_delete(aParams->wcs);
  cpl_free(aParams);
} /* muse_resampling_params_delete() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Linear inverse distance weighting function.
  @param   r   linear distance r
  @return  the weight

  Set weight to high number in case of perfect match (using FLT_MAX for this
  double variable should allow enough space for other pixel's weights to be
  added on top of this).
*/
/*---------------------------------------------------------------------------*/
static inline double
muse_resampling_weight_function_linear(double r)
{
  return r == 0 ? FLT_MAX : 1. / r;
}

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Quadratic inverse distance weighting function.
  @param   r2   squared distance r^2
  @return  the weight

  Set weight to high number in case of perfect match (using FLT_MAX for this
  double variable should allow enough space for other pixel's weights to be
  added on top of this).
*/
/*---------------------------------------------------------------------------*/
static inline double
muse_resampling_weight_function_quadratic(double r2)
{
  return r2 == 0 ? FLT_MAX : 1. / r2;
}

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Modified Shepard-like distance weighting function following Renka.
  @param   r     linear distance r
  @param   r_c   critical radius r_c beyond which the function returns 0
  @return  the weight

  Set weight to high number in case of perfect match (using FLT_MAX for this
  double variable should allow enough space for other pixel's weights to be
  added on top of this) and to a very low number in case this value is outside
  the critical radius r_c (using DBL_MIN avoids divide-by-zero).
*/
/*---------------------------------------------------------------------------*/
static inline double
muse_resampling_weight_function_renka(double r, double r_c)
{
  if (r == 0) {
    return FLT_MAX;
  } else if (r >= r_c) {
    return DBL_MIN;
  } else {
    double p = (r_c - r) / (r_c * r);
    return p*p;
  }
}

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Normalized sinc distance weighting function.
  @param   r   linear distance r
  @return  the weight
*/
/*---------------------------------------------------------------------------*/
static inline double
muse_resampling_weight_function_sinc(double r)
{
  return fabs(r) < DBL_EPSILON ? 1. : sin(CPL_MATH_PI * r) / (CPL_MATH_PI * r);
}

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Lanczos distance weighting function (restricted sinc).
  @param   dx   linear distance in x direction (spatial)
  @param   dy   linear distance in y direction (spatial)
  @param   dz   linear distance in z direction (wavelength)
  @param   n    maximum radius in pixels
  @return  the weight
*/
/*---------------------------------------------------------------------------*/
static inline double
muse_resampling_weight_function_lanczos(double dx, double dy, double dz, unsigned int n)
{
  return (fabs(dx) >= n || fabs(dy) >= n || fabs(dz) > n) ? 0.
    : muse_resampling_weight_function_sinc(dx) * muse_resampling_weight_function_sinc(dx / n)
      * muse_resampling_weight_function_sinc(dy) * muse_resampling_weight_function_sinc(dy / n)
      * muse_resampling_weight_function_sinc(dz) * muse_resampling_weight_function_sinc(dz / n);
}

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Drizzle-like distance weighting function.
  @param   xin    pixfrac-scaled input pixel size in x direction (spatial)
  @param   yin    pixfrac-scaled input pixel size in y direction (spatial)
  @param   zin    pixfrac-scaled input pixel size in z direction (wavelength)
  @param   xout   target voxel size in x direction (spatial)
  @param   yout   target voxel size in y direction (spatial)
  @param   zout   target voxel size in z direction (wavelength)
  @param   dx     offset from target voxel in x direction (spatial)
  @param   dy     offset from target voxel in y direction (spatial)
  @param   dz     offset from target voxel in z direction (wavelength)
  @return  the weight

  This function computes the percentage of flux of the original pixel that
  "drizzles" into the target voxel. It therefore has to be given a good estimate
  of the input size and the target size as well as the offset in all three axes.
*/
/*---------------------------------------------------------------------------*/
static inline double
muse_resampling_weight_function_drizzle(double xin, double yin, double zin,
                                        double xout, double yout, double zout,
                                        double dx, double dy, double dz)
{
  /* compute the three terms in the numerator: if the offset   *
   * plus the output halfsize is less than the input halfsize, *
   * then that side is fully contained in the input pixel      */
  double x = (dx + xout / 2.) <= xin / 2. ? xout : (xin + xout) / 2. - dx,
         y = (dy + yout / 2.) <= yin / 2. ? yout : (yin + yout) / 2. - dy,
         z = (dz + zout / 2.) <= zin / 2. ? zout : (zin + zout) / 2. - dz;
  /* any negative value means that the input pixel is completely *
   * outside the target voxel, so it doesn't contribute          */
  if (x <= 0 || y <= 0 || z <= 0) {
    return 0.;
  }
  /* any value > input size means this dimension of the input pixel *
   * is completely inside the target voxel, so that's the limit!    */
  return (x > xin ? xin : x) * (y > yin ? yin : y) * (z > zin ? zin : z)
         / (xin * yin * zin);
} /* muse_resampling_weight_function_drizzle() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Do the resampling from pixel grid into 3D cube using nearest
           neighbor.
  @param   aCube       the MUSE datacube to fill
  @param   aPixtable   the input pixel table
  @param   aGrid       the input pixel grid
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  Simply loop through all grid points wavelength by wavelength and set the data,
  dq, and stat values of the one or closest data point.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aCube\, aPixtable\, or aGrid are NULL}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_cube_nearest(muse_datacube *aCube, muse_pixtable *aPixtable,
                             muse_pixgrid *aGrid)
{
  cpl_ensure_code(aCube && aPixtable && aGrid, CPL_ERROR_NULL_INPUT);
  double ptxoff = 0., /* zero by default ...   */
         ptyoff = 0., /* for pixel coordinates */
         crval3 = muse_pfits_get_crval(aCube->header, 3),
         crpix3 = muse_pfits_get_crpix(aCube->header, 3),
         cd33 = muse_pfits_get_cd(aCube->header, 3, 3);
  const char *ctype3 = muse_pfits_get_ctype(aCube->header, 3);
  muse_wcs *wcs = muse_wcs_new(aCube->header);
  wcs->iscelsph = muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH;
  cpl_boolean loglambda = ctype3 && (!strncmp(ctype3, "AWAV-LOG", 9) ||
                                     !strncmp(ctype3, "WAVE-LOG", 9));
  float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
        *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA),
        *stat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT);
  int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);

  /* If our data was astrometrically calibrated, we need to scale the *
   * data units to the pixel size in all three dimensions so that the *
   * radius computation works again.                                  *
   * Otherwise dx~5.6e-5deg won't contribute to the weighting at all. */
  double xnorm = 1., ynorm = 1., znorm = 1. / kMuseSpectralSamplingA;
  if (wcs->iscelsph) {
    muse_wcs_get_scales(aPixtable->header, &xnorm, &ynorm);
    xnorm = 1. / xnorm;
    ynorm = 1. / ynorm;
    /* need to use the real coordinate offset for celestial spherical */
    ptxoff = muse_pfits_get_crval(aPixtable->header, 1);
    ptyoff = muse_pfits_get_crval(aPixtable->header, 2);
  }

#ifdef ESO_ENABLE_DEBUG
  int debug = 0;
  if (getenv("MUSE_DEBUG_NEAREST")) {
    debug = atoi(getenv("MUSE_DEBUG_NEAREST"));
  }
#endif

  int l;
#ifdef ESO_ENABLE_DEBUG
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aCube, aGrid, cd33, crpix3, crval3, data, debug, dq, lbda,    \
                 loglambda, ptxoff, ptyoff, stat, stdout, wcs, xnorm, xpos,    \
                 ynorm, ypos, znorm)
#else
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aCube, aGrid, cd33, crpix3, crval3, data, dq, lbda,           \
                 loglambda, ptxoff, ptyoff, stat, stdout, wcs, xnorm, xpos,    \
                 ynorm, ypos, znorm)
#endif
  for (l = 0; l < aGrid->nz; l++) {
    float *pdata = cpl_image_get_data_float(cpl_imagelist_get(aCube->data, l)),
          *pstat = cpl_image_get_data_float(cpl_imagelist_get(aCube->stat, l));
    int *pdq = cpl_image_get_data_int(cpl_imagelist_get(aCube->dq, l));
    /* wavelength of center of current grid cell (l is index starting at 0) */
    double lambda = (l + 1. - crpix3) * cd33 + crval3;
    if (loglambda) {
      lambda = crval3 * exp((l + 1. - crpix3) * cd33 / crval3);
    }

    int i;
    for (i = 0; i < aGrid->nx; i++) {
      int j;
      for (j = 0; j < aGrid->ny; j++) {
        cpl_size idx = muse_pixgrid_get_index(aGrid, i, j, l, CPL_FALSE),
                 n_rows = muse_pixgrid_get_count(aGrid, idx);
        const cpl_size *rows = muse_pixgrid_get_rows(aGrid, idx);

        /* x and y position of center of current grid cell (i, j start at 0) */
        double x, y;
        if (wcs->iscelsph) {
          muse_wcs_celestial_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
        } else {
          muse_wcs_projplane_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
        }

        if (n_rows == 1) {
          /* if there is only one pixel in the cell, just use it */
          pdata[i + j * aGrid->nx] = data[rows[0]];
          pstat[i + j * aGrid->nx] = stat[rows[0]];
          pdq[i + j * aGrid->nx] = dq[rows[0]];

#ifdef ESO_ENABLE_DEBUG
          if (debug) {
            printf("only:    %f,%f\n", data[rows[0]], stat[rows[0]]);
            fflush(stdout);
          }
#endif
        } else if (n_rows >= 2) {
          /* loop through all available values and take the closest one */
          cpl_size n, nbest = -1;
          double dbest = FLT_MAX; /* some unlikely large value to start with*/
          for (n = 0; n < n_rows; n++) {
            /* the differences for the cell center and the current pixel */
            double dx = fabs(x - xpos[rows[n]] + ptxoff) * xnorm,
                   dy = fabs(y - ypos[rows[n]] + ptyoff) * ynorm,
                   dlambda = fabs(lambda - lbda[rows[n]]) * znorm,
                   dthis = sqrt(dx*dx + dy*dy + dlambda*dlambda);
            if (wcs->iscelsph) {
              /* Not strictly necessary for NN, but still scale the RA   *
               * distance properly, see muse_resampling_cube_weighted(). */
              dx *= cos(y * CPL_MATH_RAD_DEG);
            }
#ifdef ESO_ENABLE_DEBUG
            if (debug) {
              printf("%f %f %f, %f %f %f, d: %f %f %f -> %f best: %f (%f,%f)\n",
                     x, y, lambda, xpos[rows[n]] + ptxoff, ypos[rows[n]] + ptyoff,
                     lbda[rows[n]], dx, dy, dlambda, dthis, dbest, data[rows[n]],
                     stat[rows[n]]);
            }
#endif
            if (dthis < dbest) {
              nbest = n;
              dbest = dthis;
            }
          }
          pdata[i + j * aGrid->nx] = data[rows[nbest]];
          pstat[i + j * aGrid->nx] = stat[rows[nbest]];
          pdq[i + j * aGrid->nx] = dq[rows[nbest]];
#ifdef ESO_ENABLE_DEBUG
          if (debug) {
            printf("closest: %f,%f\n", data[rows[nbest]], stat[rows[nbest]]);
            fflush(stdout);
          }
#endif
        } else {
          /* npix == 0: do nothing, pixel stays zero */
          pdq[i + j * aGrid->nx] = EURO3D_MISSDATA;
        }
      } /* for j (y direction) */
    } /* for i (x direction) */
  } /* for l (wavelength planes) */
  cpl_free(wcs);

  return CPL_ERROR_NONE;
} /* muse_resampling_cube_nearest() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Do the resampling from pixel grid into 3D cube using a weighting
           scheme.
  @param   aCube       the MUSE datacube to fill
  @param   aPixtable   the input pixel table
  @param   aGrid       the input pixel grid
  @param   aParams     the structure of resampling parameters
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  Loop through all grid points wavelength by wavelength and compute the final
  values (of data/flux, dq, and stat) using a suitable weighting scheme on the
  surrounding grid points.

  All parameters are set in the aParams structure.

  @error{set and return CPL_ERROR_NULL_INPUT,
         aCube\, aPixtable\, aGrid\, or aParams are NULL}
  @error{override it and output warning, loop distance (aParams->ld) is <= 0}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_cube_weighted(muse_datacube *aCube, muse_pixtable *aPixtable,
                              muse_pixgrid *aGrid,
                              muse_resampling_params *aParams)
{
  cpl_ensure_code(aCube && aPixtable && aGrid && aParams,
                  CPL_ERROR_NULL_INPUT);
  double ptxoff = 0., /* zero by default ...   */
         ptyoff = 0., /* for pixel coordinates */
         crval3 = muse_pfits_get_crval(aCube->header, 3),
         crpix3 = muse_pfits_get_crpix(aCube->header, 3),
         cd33 = muse_pfits_get_cd(aCube->header, 3, 3);
  const char *ctype3 = muse_pfits_get_ctype(aCube->header, 3);
  muse_wcs *wcs = muse_wcs_new(aCube->header);
  wcs->iscelsph = muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH;
  cpl_boolean loglambda = ctype3 && (!strncmp(ctype3, "AWAV-LOG", 9) ||
                                     !strncmp(ctype3, "WAVE-LOG", 9));
  cpl_errorstate prestate = cpl_errorstate_get();
  float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
        *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA),
        *stat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT),
        *wght = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_WEIGHT);
  if (!cpl_errorstate_is_equal(prestate)) {
    cpl_errorstate_set(prestate); /* recover from missing weight column */
  }
  int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ),
      *orgn = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_ORIGIN);

  /* If our data was astrometrically calibrated, we need to scale the *
   * data units to the pixel size in all three dimensions so that the *
   * radius computation works again.                                  *
   * Otherwise dx~5.6e-5deg won't contribute to the weighting at all. */
  double xnorm = 1., ynorm = 1., znorm = 1. / kMuseSpectralSamplingA;
  if (wcs->iscelsph) {
    muse_wcs_get_scales(aPixtable->header, &xnorm, &ynorm);
    xnorm = 1. / xnorm;
    ynorm = 1. / ynorm;
    /* need to use the real coordinate offset for celestial spherical */
    ptxoff = muse_pfits_get_crval(aPixtable->header, 1);
    ptyoff = muse_pfits_get_crval(aPixtable->header, 2);
  }
  /* precomputed factor for output voxel size calculation in *
   * wavelength direction, only needed for log-lambda axis   */
  double zoutefac = exp(1.5 * cd33 / crval3) - exp(0.5 * cd33 / crval3);
  /* scale the input critical radius by the voxel radius */
  double renka_rc = aParams->rc                    /* XXX beware of rotation! */
    * sqrt((wcs->cd11*xnorm)*(wcs->cd11*xnorm) + (wcs->cd22*ynorm)*(wcs->cd22*ynorm)
           + (cd33*znorm)*(cd33*znorm));
  /* loop distance (to take into account surrounding pixels) verification */
  int ld = aParams->ld;
  if (ld <= 0) {
    ld = 1;
    cpl_msg_info(__func__, "Overriding loop distance ld=%d", ld);
  }

  /* pixel sizes in all three directions, scaled by pixfrac, and *
   * output pixel sizes (absolute values), as needed for drizzle */
  double xsz = aParams->pfx / xnorm,
         ysz = aParams->pfy / ynorm,
         zsz = aParams->pfl / znorm,
         xout = fabs(wcs->cd11), yout = fabs(wcs->cd22), zout = fabs(cd33);

  /* set up masking cube to create a bad pixel table */
  char *envmaskcube = getenv("MUSE_BADPIX_FROM_MASKCUBE"),
       *badpix = NULL,
       *maskname = NULL;
  cpl_table *bpt = NULL;
  cpl_imagelist *maskcube = NULL;
  if (envmaskcube) {
    /* try to get both filenames from the variable */
    cpl_array *files = muse_cplarray_new_from_delimited_string(envmaskcube, ",");
    int nfiles = cpl_array_get_size(files);
    if (nfiles >= 2) {
      maskname = cpl_strdup(cpl_array_get_string(files, 0));
      badpix = cpl_strdup(cpl_array_get_string(files, 1));
    } else if (nfiles == 1) {
      maskname = cpl_strdup(cpl_array_get_string(files, 0));
      badpix = cpl_sprintf("BADPIX_FROM_MASKCUBE.fits");
      cpl_msg_warning(__func__, "MUSE_BADPIX_FROM_MASKCUBE with only one "
                      "argument, using \"%s\" forthe bad pixel table", badpix);
    } else {
      cpl_msg_warning(__func__, "MUSE_BADPIX_FROM_MASKCUBE without arguments, "
                      "not creating a mask table!");
    }
    if (maskname) {
      maskcube = cpl_imagelist_load(maskname, CPL_TYPE_INT, 0);
    }
    /* check against pixel grid size */
    if (maskcube) {
      int xsize = cpl_image_get_size_x(cpl_imagelist_get(maskcube, 0)),
          ysize = cpl_image_get_size_y(cpl_imagelist_get(maskcube, 0)),
          zsize = cpl_imagelist_get_size(maskcube);
      if (zsize != aGrid->nz) {
        cpl_msg_warning(__func__, "Invalid z size of mask cube (%d vs %d)",
                        zsize, (int)aGrid->nz);
        cpl_imagelist_delete(maskcube);
        maskcube = NULL;
      } else if (xsize != aGrid->nx) {
        cpl_msg_warning(__func__, "Invalid x size of mask cube (%d vs %d)",
                        xsize, (int)aGrid->nx);
        cpl_imagelist_delete(maskcube);
        maskcube = NULL;
      } else if (ysize != aGrid->ny) {
        cpl_msg_warning(__func__, "Invalid y size of mask cube (%d vs %d)",
                        ysize, (int)aGrid->ny);
        cpl_imagelist_delete(maskcube);
        maskcube = NULL;
      } else {
        cpl_msg_info(__func__, "Mask cube with valid size found (%d x %d x %d)",
                     xsize, ysize, zsize);
      }
    } /* if maskcube */

    /* if the cube is still there, it should be valid, so *
     * create output bad pixel table                      */
    if (maskcube) {
      bpt = muse_cpltable_new(muse_badpix_table_def, 0);
      /* add a few temporary columns to the standard bad pixel table */
      cpl_table_new_column(bpt, "pweight", CPL_TYPE_DOUBLE);
      cpl_table_new_column(bpt, "ifu", CPL_TYPE_INT);
    } else {
      cpl_msg_warning(__func__, "No mask cube found, not creating output bad "
                      "pixel table!");
    }
    cpl_array_delete(files);
  } /* if envmaskcube */

#ifdef ESO_ENABLE_DEBUG
  int debug = 0, debugx = 0, debugy = 0, debugz = 0;
  if (getenv("MUSE_DEBUG_WEIGHTED")) {
    debug = atoi(getenv("MUSE_DEBUG_WEIGHTED"));
  }
  if (debug & 2) { /* need coordinates */
    if (getenv("MUSE_DEBUG_WEIGHTED_X")) {
      debugx = atoi(getenv("MUSE_DEBUG_WEIGHTED_X"));
      if (debugx < 1 || debugx > aGrid->nx) {
        debugx = 0;
      }
    }
    if (getenv("MUSE_DEBUG_WEIGHTED_Y")) {
      debugy = atoi(getenv("MUSE_DEBUG_WEIGHTED_Y"));
      if (debugy < 1 || debugy > aGrid->ny) {
        debugy = 0;
      }
    }
    if (getenv("MUSE_DEBUG_WEIGHTED_Z")) {
      debugz = atoi(getenv("MUSE_DEBUG_WEIGHTED_Z"));
      if (debugz < 1 || debugz > aGrid->nz) {
        debugz = 0;
      }
    }
  }
  if (debug & 8) {
    printf("parameters:\n            cd=%e %e %e\n"
           "  corrected crpix3=%e\n             norm=%e %e %e\n",
           wcs->cd11, wcs->cd22, cd33, crpix3, xnorm, ynorm, znorm);
    if (aParams->method == MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
      printf("  drop sizes %e %e %e (pixfrac %f,%f,%f)\n"
             "  output sizes %e %e %e\n",
             xsz, ysz, zsz, aParams->pfx, aParams->pfy, aParams->pfl,
             xout, yout, zout);
    } else {
      printf("  resulting renka_rc: %e %e %e / %e %e %e --> %e\n",
             pow(wcs->cd11, 2), pow(wcs->cd22, 2), cd33*cd33,
             pow(wcs->cd11*xnorm, 2), pow(wcs->cd22*ynorm, 2),
             pow(cd33*znorm, 2), renka_rc);
    }
    fflush(stdout);
  }
#endif
  cpl_imagelist *wcube = NULL;
  if (getenv("MUSE_DEBUG_WEIGHT_CUBE")) { /* create a weight cube */
    cpl_msg_debug(__func__, "Weighted resampling: creating weight cube");
    wcube = cpl_imagelist_new();
    int i;
    for (i = 0; i < aGrid->nz; i++) {
      cpl_image *image = cpl_image_new(aGrid->nx, aGrid->ny,
                                       CPL_TYPE_FLOAT);
      cpl_imagelist_set(wcube, image, i);
    } /* for i (all wavelength planes) */
  } /* if weight cube */

  if (getenv("MUSE_DEBUG_WEIGHTED_GRID")) {
    char *fn = getenv("MUSE_DEBUG_WEIGHTED_GRID");
    FILE *grid = fopen(fn, "w");
    if (grid) {
      cpl_msg_info(__func__, "Writing grid to \"%s\"", fn);
      fprintf(grid, "# i   j    l    x              y            lambda\n");
      int l;
      for (l = 0; l < aGrid->nz; l++) {
        double lambda = (l + 1. - crpix3) * cd33 + crval3;
        int i;
        for (i = 0; i < aGrid->nx; i++) {
          int j;
          for (j = 0; j < aGrid->ny; j++) {
            /* x and y position of center of current grid cell (i, j start at 0) */
            double x, y;
            if (wcs->iscelsph) {
              muse_wcs_celestial_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
            } else {
              muse_wcs_projplane_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
            }
            fprintf(grid, "%03d %03d %04d %.10f %.10f %8.3f\n", i+1, j+1, l+1,
                    x, y, lambda);
          } /* for j (y direction) */
        } /* for i (x direction) */
      } /* for l (wavelength planes) */
      fclose(grid);
    } else {
      cpl_msg_warning(__func__, "Writing grid to \"%s\" failed!", fn);
    }
  } /* if grid output */

  int l;
#ifdef ESO_ENABLE_DEBUG
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aCube, aParams, aGrid, aPixtable, bpt, cd33, crpix3,          \
                 crval3, data, debug, debugx, debugy, debugz, dq, lbda, ld,    \
                 loglambda, maskcube, orgn, ptxoff, ptyoff, renka_rc, stat,    \
                 stdout, wcs, wcube, wght,  xnorm, xout, xpos, xsz, ynorm,     \
                 yout, ypos, ysz, znorm, zout, zoutefac, zsz)
#else
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aCube, aParams, aGrid, aPixtable, bpt, cd33, crpix3,          \
                 crval3, data, dq, lbda, ld, loglambda, maskcube, orgn, ptxoff,\
                 ptyoff, renka_rc, stat, stdout, wcs, wcube, wght, xnorm, xout,\
                 xpos, xsz, ynorm, yout, ypos, ysz, znorm, zout, zoutefac, zsz)
#endif
  for (l = 0; l < aGrid->nz; l++) {
    float *pdata = cpl_image_get_data_float(cpl_imagelist_get(aCube->data, l)),
          *pstat = cpl_image_get_data_float(cpl_imagelist_get(aCube->stat, l));
    int *pdq = cpl_image_get_data_int(cpl_imagelist_get(aCube->dq, l));
    /* wavelength of center of current grid cell (l is index starting at 0) */
    double lambda = (l + 1. - crpix3) * cd33 + crval3;
    double zout2 = zout; /* correct the output pixel size for log-lambda */
    if (loglambda) {
      lambda = crval3 * exp((l + 1. - crpix3) * cd33 / crval3);
      zout2 = crval3 * exp((l - crpix3) * cd33 / crval3) * zoutefac;
    }
    float *pwcube = NULL;
    if (wcube) { /* weight cube */
      pwcube = cpl_image_get_data_float(cpl_imagelist_get(wcube, l));
    } /* if weight cube */

    cpl_image *mask = NULL;
    if (maskcube) {
      mask = cpl_imagelist_get(maskcube, l);
    }

    int i;
    for (i = 0; i < aGrid->nx; i++) {
      int j;
      for (j = 0; j < aGrid->ny; j++) {
        /* x and y position of center of current grid cell (i, j start at 0) */
        double x, y;
        if (wcs->iscelsph) {
          muse_wcs_celestial_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
        } else {
          muse_wcs_projplane_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
        }
        double sumdata = 0, sumstat = 0, sumweight = 0;
        int npoints = 0;
#ifdef ESO_ENABLE_DEBUG
        cpl_size *pointlist = NULL;
        double *pointweights = NULL;
        int npointlist = 0;
        if (debug & 2 && i+1 == debugx && j+1 == debugy && l+1 == debugz) {
          pointlist = cpl_calloc(100, sizeof(cpl_size));
          pointweights = cpl_malloc(100 * sizeof(double));
          npointlist = 100;
        }
#endif
        /* set up point list for bad pixel table writing */
        cpl_size *plist = NULL;
        double *pweights = NULL;
        int nplist = 0,
            err;
        if (mask && cpl_image_get(mask, i+1, j+1, &err) != 0) {
          plist = cpl_calloc(100, sizeof(cpl_size));
          pweights = cpl_malloc(100 * sizeof(double));
          nplist = 100;
        }

#ifdef ESO_ENABLE_DEBUG
        if (debug & 1) {
          printf("cell %d %d %d:\n", i, j, l);
        }
#endif
        /* loop through surrounding cells and their contained pixels */
        int i2;
        for (i2 = i - ld; i2 <= i + ld; i2++) {
          int j2;
          for (j2 = j - ld; j2 <= j + ld; j2++) {
            int l2;
            for (l2 = l - ld; l2 <= l + ld; l2++) {
              cpl_size idx2 = muse_pixgrid_get_index(aGrid, i2, j2, l2, CPL_FALSE),
                       n_rows2 = muse_pixgrid_get_count(aGrid, idx2);
#ifdef ESO_ENABLE_DEBUG
              if (debug & 8 && n_rows2 < 1) {
                printf("%d %d %d / %d %d %d (%"CPL_SIZE_FORMAT"): no rows!\n",
                       i+1, j+1, l+1, i2+1, j2+1, l2+1, idx2);
              }
#endif
              const cpl_size *rows2 = muse_pixgrid_get_rows(aGrid, idx2);
              cpl_size n;
              for (n = 0; n < n_rows2; n++) {
                if (dq[rows2[n]]) { /* exclude all bad pixels */
#ifdef ESO_ENABLE_DEBUG
                  if (debug & 8) {
                    printf("%d %d %d / %d %d %d (%"CPL_SIZE_FORMAT", "
                           "%"CPL_SIZE_FORMAT"): bad!\n",
                           i+1, j+1, l+1, i2+1, j2+1, l2+1, idx2, n);
                    fflush(stdout);
                  }
#endif
                  continue;
                }

                double dx = fabs(x - (xpos[rows2[n]] + ptxoff)),
                       dy = fabs(y - (ypos[rows2[n]] + ptyoff)),
                       dlambda = fabs(lambda - lbda[rows2[n]]),
                       r2 = 0;
                if (wcs->iscelsph) {
                  /* Since the distances of RA in degrees get larger the *
                   * closer we get to the celestial pole, we have to     *
                   * compensate for that by multiplying the distance in  *
                   * RA by cos(delta), to make it comparable to the      *
                   * distances in pixels for the differnt kernels below. */
                  dx *= cos(y * CPL_MATH_RAD_DEG);
                }
                if (aParams->method != MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
                  dx *= xnorm;
                  dy *= ynorm;
                  dlambda *= znorm;
                  r2 = dx*dx + dy*dy + dlambda*dlambda;
                }
                double weight = 0.;
                if (aParams->method == MUSE_RESAMPLE_WEIGHTED_RENKA) {
                  weight = muse_resampling_weight_function_renka(sqrt(r2), renka_rc);
                } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
                  weight = muse_resampling_weight_function_drizzle(xsz, ysz, zsz,
                                                                   xout, yout, zout2,
                                                                   dx, dy, dlambda);
                } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_LINEAR) {
                  weight = muse_resampling_weight_function_linear(sqrt(r2));
                } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_QUADRATIC) {
                  weight = muse_resampling_weight_function_quadratic(r2);
                } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_LANCZOS) {
                  weight = muse_resampling_weight_function_lanczos(dx, dy, dlambda, ld);
                }

                if (wght) { /* the pixel table does contain weights */
                  /* apply it on top of the weight computed here */
                  weight *= wght[rows2[n]];
                }
#ifdef ESO_ENABLE_DEBUG
                if (debug & 8) {
                  printf("%d %d %d / %d %d %d (%"CPL_SIZE_FORMAT", %"CPL_SIZE_FORMAT"):"
                         "  x %e %e %e %e  y %e %e %e %e  l %e %e %e %e --> %e %e %e\n",
                         i+1, j+1, l+1, i2+1, j2+1, l2+1, idx2, n,
                         x, xpos[rows2[n]]+ptxoff, fabs(x - (xpos[rows2[n]]+ptxoff)), dx,
                         y, ypos[rows2[n]]+ptyoff, fabs(y - (ypos[rows2[n]]+ptyoff)), dy,
                         lambda, lbda[rows2[n]], fabs(lambda - lbda[rows2[n]]), dlambda,
                         r2, sqrt(r2), weight);
                  fflush(stdout);
                }
#endif
                sumweight += weight;
                sumdata += data[rows2[n]] * weight;
                sumstat += stat[rows2[n]] * weight*weight;
                npoints++;
#ifdef ESO_ENABLE_DEBUG
                if (debug & 2 && i+1 == debugx && j+1 == debugy && l+1 == debugz) {
                  if (npoints > npointlist) {
                    pointlist = cpl_realloc(pointlist,
                                            (npointlist + 100) * sizeof(cpl_size));
                    memset(pointlist + npointlist, 0, 100 * sizeof(cpl_size));
                    pointweights = cpl_realloc(pointweights,
                                               (npointlist + 100) * sizeof(double));
                    npointlist += 100;
                  }
                  /* store row number instead of index, because we cannot be zero: */
                  pointlist[npoints-1] = rows2[n] + 1;
                  pointweights[npoints-1] = weight;
                }

                if (debug & 4) {
                  cpl_size idx = muse_pixgrid_get_index(aGrid, i, j, l, CPL_FALSE),
                           count = muse_pixgrid_get_count(aGrid, idx);
                  if (count) {
                    printf("  pixel %4d,%4d,%4d (%8"CPL_SIZE_FORMAT"): "
                           "%2"CPL_SIZE_FORMAT" %2"CPL_SIZE_FORMAT"   %f %f %f, "
                           " %e -> %e ==> %e %e (%d)\n", i+1, j+1, l+1, idx,
                           n, count, dx, dy, dlambda,
                           data[muse_pixgrid_get_rows(aGrid, idx)[n]],
                           weight, sumweight, sumdata, npoints);
                  }
                }
#endif
                /* fill into point list for bad pixel table writing */
                if (plist && pweights) {
                  if (npoints > nplist) {
                    plist = cpl_realloc(plist, (nplist + 100) * sizeof(cpl_size));
                    memset(plist + nplist, 0, 100 * sizeof(cpl_size));
                    pweights = cpl_realloc(pweights, (nplist + 100) * sizeof(double));
                    nplist += 100;
                  }
                  /* store row number instead of index, because we cannot be zero: */
                  plist[npoints-1] = rows2[n] + 1;
                  pweights[npoints-1] = weight;
                } /* if plist and pweights */
              } /* for n (all pixels in grid cell) */
            } /* for l2 (lambda direction) */
          } /* for j2 (y direction) */
        } /* for i2 (x direction) */

#ifdef ESO_ENABLE_DEBUG
        if (debug & 2 && i+1 == debugx && j+1 == debugy && l+1 == debugz) {
          printf("cell center (%d, %d, %d): %14.7e %14.7e %9.3f\npixelnumber  "
                 "weight     ", debugx, debugy, debugz, x, y, lambda);
          muse_pixtable_dump(aPixtable, 0, 0, 2);
          int m = -1;
          while (++m < npointlist && pointlist[m] != 0) {
            /* access row using row index again instead of stored row number: */
            printf("%12"CPL_SIZE_FORMAT" %8.5f    ", pointlist[m] - 1,
                   pointweights[m]);
            muse_pixtable_dump(aPixtable, pointlist[m] - 1, 1, 0);
          }
          printf("sums: %g %g %g --> %g %g\n", sumdata, sumstat, sumweight,
                 sumdata / sumweight, sumstat / pow(sumweight, 2));
          cpl_free(pointlist);
          cpl_free(pointweights);
        }

        if (debug & 1 && npoints) {
          printf("  sumdata: %e %e (%d)", sumdata, sumweight, npoints);
        }
#endif
        /* evaluate point list and write to bad pixel table */
        if (plist && pweights) {
          cpl_size ibad = -1;
          #pragma omp critical(muse_bpt_size)
          {
          /* original size and first row index */
          ibad = cpl_table_get_nrow(bpt);
          /* enlarge to fit the new bad pixels */
          cpl_table_set_size(bpt, ibad + nplist);
          } /* omp critical */
          int m = -1;
          while (++m < nplist && plist[m] != 0) {
            /* access row using row index again instead of stored row number */
            cpl_size ipt = plist[m] - 1;
            int xraw = muse_pixtable_origin_get_x(orgn[ipt], aPixtable, ipt),
                yraw = muse_pixtable_origin_get_y(orgn[ipt]);
            muse_quadrants_coords_to_raw(NULL, &xraw, &yraw);
            unsigned short ifu = muse_pixtable_origin_get_ifu(orgn[ipt]);
            #pragma omp critical(muse_bpt_size)
            {
            /* now set all of them in the current table row */
            cpl_table_set_int(bpt, MUSE_BADPIX_X, ibad, xraw);
            cpl_table_set_int(bpt, MUSE_BADPIX_Y, ibad, yraw);
            cpl_table_set_int(bpt, MUSE_BADPIX_DQ, ibad, EURO3D_BADOTHER);
            cpl_table_set_double(bpt, "pweight", ibad, pweights[m]);
            cpl_table_set_int(bpt, "ifu", ibad, ifu);
            } /* omp critical */
            ibad++;
#ifdef ESO_ENABLE_DEBUG
            if (debug & (i+1 == debugx && j+1 == debugy && l+1 == debugz)) {
              printf("%12"CPL_SIZE_FORMAT" %8.5f   %02hhu %4d %4d\n",
                     ipt, pweights[m], ifu, xraw, yraw);
            }
            fflush(stdout);
#endif
          } /* while */
          cpl_free(plist);
          cpl_free(pweights);
          /* mark this voxel in the STAT extension */
          sumstat = NAN;
        } /* if plist and pweights */

        /* if no points were found, we cannot divide by the summed weight *
         * and don't need to set the output pixel value (it's 0 already), *
         * only set the relevant Euro3D bad pixel flag                    */
        if (!npoints || !isnormal(sumweight)) {
          pdq[i + j * aGrid->nx] = EURO3D_MISSDATA;
#ifdef ESO_ENABLE_DEBUG
          if (debug & 1) {
            printf(" -> no points or weird weight\n");
          }
#endif
          continue;
        }

        /* divide results by weight of summed pixels */
        sumdata /= sumweight;
        sumstat /= sumweight*sumweight;
#ifdef ESO_ENABLE_DEBUG
        if (debug & 1) {
          printf(" -> %e (%e)\n", sumdata, sumstat);
        }
        if (debug) {
          fflush(stdout); /* flush the output in any debug case */
        }
#endif
        pdata[i + j * aGrid->nx] = sumdata;
        pstat[i + j * aGrid->nx] = sumstat;
        pdq[i + j * aGrid->nx] = EURO3D_GOODPIXEL; /* now we can mark it as good */
        if (pwcube) {
          pwcube[i + j * aGrid->nx] = sumweight;
        } /* if weight cube */
      } /* for j (y direction) */
    } /* for i (x direction) */
  } /* for l (wavelength planes) */
  cpl_free(wcs);

  if (wcube) { /* weight cube */
    const char *fn = getenv("MUSE_DEBUG_WEIGHT_CUBE");
    cpl_error_code rc = cpl_imagelist_save(wcube, fn, CPL_TYPE_UNSPECIFIED,
                                           NULL, CPL_IO_CREATE);
    if (rc != CPL_ERROR_NONE) {
      cpl_msg_warning(__func__, "Failure to save weight cube as \"%s\": %s", fn,
                      cpl_error_get_message());
    } else {
      cpl_msg_info(__func__, "Saved weight cube as \"%s\"", fn);
    }
    cpl_imagelist_delete(wcube);
  } /* if weight cube */

  /* clean up after the mask-cube and the new bad pixel table, *
   * but first write that to disk                              */
  if (badpix) {
    unsigned char nifu;
    cpl_boolean isfirst = CPL_TRUE;
    for (nifu = 1; nifu <= kMuseNumIFUs; nifu++) {
      /* select all good rows belonging to this IFU with positive weight */
      cpl_table_unselect_all(bpt);
      cpl_table_or_selected_int(bpt, "ifu", CPL_EQUAL_TO, nifu);
      cpl_table_and_selected_double(bpt, "pweight", CPL_GREATER_THAN, 0.);

      /* extract the relevant rows for this IFU, and save the table */
      cpl_table *bptifu = cpl_table_extract_selected(bpt);
      cpl_size nbad = cpl_table_get_nrow(bptifu);
      if (nbad < 1) {
        cpl_msg_info(__func__, "No bad pixels from mask in IFU %02hhu, "
                     "nothing saved.", nifu);
        cpl_table_delete(bptifu);
        continue;
      }

      /* sort and "uniquify" the table rows */
      cpl_propertylist *sorting = cpl_propertylist_new();
      cpl_propertylist_append_bool(sorting, MUSE_BADPIX_X, CPL_FALSE);
      cpl_propertylist_append_bool(sorting, MUSE_BADPIX_Y, CPL_FALSE);
      cpl_table_sort(bptifu, sorting);
      cpl_propertylist_delete(sorting);
      /* now loop through all rows, and compare with the previous row *
       * and select duplicate coordinates                             */
      cpl_table_unselect_all(bptifu);
      int irow,
          xprev = cpl_table_get_int(bptifu, MUSE_BADPIX_X, 0, NULL),
          yprev = cpl_table_get_int(bptifu, MUSE_BADPIX_Y, 0, NULL);
      for (irow = 1; irow < nbad; irow++) {
        int x = cpl_table_get_int(bptifu, MUSE_BADPIX_X, irow, NULL),
            y = cpl_table_get_int(bptifu, MUSE_BADPIX_Y, irow, NULL);
        if (x == xprev && y == yprev) {
          cpl_table_select_row(bptifu, irow);
          continue; /* do not move the comparison values */
        }
        /* replace the comparison values with the current row */
        xprev = x;
        yprev = y;
      } /* for irow (all but the first table rows) */
      cpl_table_erase_selected(bptifu);
      /* also remove the extra columns again, they *
       * should not appear in the output table     */
      cpl_table_erase_column(bptifu, "pweight");
      cpl_table_erase_column(bptifu, "ifu");

      /* save the table                                             *
       * and propagate a few important headers from the pixel table */
      cpl_propertylist *header = cpl_propertylist_new(),
                       *pheader = NULL;
      char *chan = cpl_sprintf("CHAN%02hhu", nifu);
      cpl_propertylist_append_string(header, "EXTNAME", chan);
      if (isfirst) {
        pheader = cpl_propertylist_new();
        cpl_propertylist_append_string(pheader, "INSTRUME", "MUSE");
        cpl_propertylist_append_double(pheader, "MJD-OBS",
                                       muse_pfits_get_mjdobs(aPixtable->header));
        cpl_propertylist_append_string(pheader, "DATE-OBS",
                                       muse_pfits_get_dateobs(aPixtable->header));
        cpl_propertylist_append_string(pheader, "ESO DRS MUSE MASKCUBE",
                                       maskname);
      } /* if isfirst */
      cpl_table_save(bptifu, pheader, header, badpix,
                     isfirst ? CPL_IO_CREATE : CPL_IO_EXTEND);
      cpl_free(chan);
      cpl_propertylist_delete(header);
      cpl_propertylist_delete(pheader); /* NULL check in there */
      cpl_table_delete(bptifu);
      cpl_msg_info(__func__, "Bad pixel table from mask cube for IFU %02hhu "
                   "saved to \"%s\" (%"CPL_SIZE_FORMAT" pixels).", nifu,
                   badpix, nbad);
      isfirst = CPL_FALSE;
    } /* for all IFUs */
    cpl_table_delete(bpt);

    /* also sign the table "product"; do this manually here until *
     * (if ever) it is made a full pipeline product               */
    cpl_frameset *fset = cpl_frameset_new();
    cpl_frame *frame = cpl_frame_new();
    cpl_frame_set_filename(frame, badpix);
    cpl_frame_set_tag(frame, MUSE_TAG_BADPIX_TABLE);
    cpl_frame_set_type(frame, CPL_FRAME_TYPE_TABLE);
    cpl_frame_set_group(frame, CPL_FRAME_GROUP_PRODUCT);
    cpl_frame_set_level(frame, CPL_FRAME_LEVEL_FINAL);
    cpl_frameset_insert(fset, frame);
    cpl_dfs_sign_products(fset, CPL_DFS_SIGNATURE_DATAMD5 | CPL_DFS_SIGNATURE_CHECKSUM);
    cpl_frameset_delete(fset);
  } /* if badpix */
  cpl_free(maskname);
  cpl_free(badpix);
  cpl_imagelist_delete(maskcube);

  return CPL_ERROR_NONE;
} /* muse_resampling_cube_weighted() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Adapt sampling and grid size depending on data and defaults.
  @param   aPixtable   the input pixel table
  @param   aParams     the structure of resampling parameters
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  @error{set and return CPL_ERROR_NULL_INPUT, aPixtable or aParams are NULL}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_check_deltas(muse_pixtable *aPixtable,
                             muse_resampling_params *aParams)
{
  cpl_ensure_code(aPixtable && aParams, CPL_ERROR_NULL_INPUT);
  const char func[] = "muse_resampling_cube"; /* pretend to be in that fct... */

  /* wavelength direction */
  if (aParams->dlambda == 0.0) {
    aParams->dlambda = kMuseSpectralSamplingA;
    if (aParams->tlambda == MUSE_RESAMPLING_DISP_AWAV_LOG ||
        aParams->tlambda == MUSE_RESAMPLING_DISP_WAVE_LOG) {
      aParams->dlambda /= 1.6; /* XXX seems to be a reasonable value... */
    }
  }

  if (muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_PIXEL) {
    /* if pixel table in pixel units, take a shortcut */
    if (aParams->dx == 0.0) {
      aParams->dx = 1.0;
    }
    if (aParams->dy == 0.0) {
      aParams->dy = 1.0;
    }
    cpl_msg_debug(func, "steps from parameters: %.2f pix, %.2f pix, %.3f Angstrom",
                  aParams->dx, aParams->dy, aParams->dlambda);
    return CPL_ERROR_NONE;
  }
  if (aParams->dx == 0.0) {
    aParams->dx = kMuseSpaxelSizeX_WFM / 3600.;
    if (muse_pfits_get_mode(aPixtable->header) == MUSE_MODE_NFM_AO_N) {
      aParams->dx = kMuseSpaxelSizeX_NFM / 3600.;
    }
  } else {
    /* convert from arcsec to degrees */
    aParams->dx /= 3600.;
  }
  if (aParams->dy == 0.0) {
    aParams->dy = kMuseSpaxelSizeY_WFM / 3600.;
    if (muse_pfits_get_mode(aPixtable->header) == MUSE_MODE_NFM_AO_N) {
      aParams->dy = kMuseSpaxelSizeY_NFM / 3600.;
    }
  } else {
    /* anything else should be interpreted as arcsec, but we need deg */
    aParams->dy /= 3600.;
  }
  cpl_msg_debug(func, "steps from parameters: %f arcsec, %f arcsec, %.3f Angstrom",
                aParams->dx * 3600., aParams->dy * 3600., aParams->dlambda);
  return CPL_ERROR_NONE;
} /* muse_resampling_check_deltas() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Compute "natural" cube size from the data.
  @param   aPixtable   the pixel table with the data
  @param   aParams     the resampling parameters
  @param   aX          pointer to horizontal cube size to update
  @param   aY          pointer to vertical cube size to update
  @param   aZ          pointer to dispersion direction cube size to update
  @return  CPL_ERROR_NONE on non-critical failure or success, another CPL error
           code otherwise.
  @error{return CPL_ERROR_NULL_INPUT,
         aPixtable\, aParams\, aX\, aY\, or aZ are NULL}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_compute_size(muse_pixtable *aPixtable,
                             muse_resampling_params *aParams,
                             int *aX, int *aY, int *aZ)
{
  cpl_ensure_code(aPixtable && aParams && aX && aY && aZ, CPL_ERROR_NULL_INPUT);
  const char func[] = "muse_resampling_cube"; /* pretend to be in that fct... */
  double x1, y1, x2, y2;
  float xmin = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XLO),
        xmax = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XHI),
        ymin = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YLO),
        ymax = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YHI);
  muse_pixtable_wcs wcstype = muse_pixtable_wcs_check(aPixtable);
  if (wcstype == MUSE_PIXTABLE_WCS_CELSPH) {
    muse_wcs_projplane_from_celestial(aPixtable->header, xmin, ymin, &x1, &y1);
    muse_wcs_projplane_from_celestial(aPixtable->header, xmax, ymax, &x2, &y2);
  } else {
    muse_wcs_projplane_from_pixel(aPixtable->header, xmin, ymin, &x1, &y1);
    muse_wcs_projplane_from_pixel(aPixtable->header, xmax, ymax, &x2, &y2);
  }
  *aX = lround(fabs(x2 - x1) / aParams->dx) + 1;
  *aY = lround(fabs(y2 - y1) / aParams->dy) + 1;
  float lmin = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LLO),
        lmax = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LHI);
  *aZ = (int)ceil((lmax - lmin) / aParams->dlambda) + 1;
  if (aParams->tlambda == MUSE_RESAMPLING_DISP_AWAV_LOG ||
      aParams->tlambda == MUSE_RESAMPLING_DISP_WAVE_LOG) {
    *aZ = (int)ceil(lmin / aParams->dlambda * log(lmax / lmin)) + 1;
  }
  cpl_msg_info(func, "Output cube size %d x %d x %d (fit to data)",
               *aX, *aY, *aZ);
  return CPL_ERROR_NONE;
} /* muse_resampling_compute_size() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Override integer WCS property and output message.
  @param   aV         pointer to value to be changed
  @param   aKeyword   FITS keyword name to output
  @param   aValue     the value to replace with
  @error{return, aValue is non-positive}
  @error{return, aV is NULL}
 */
/*---------------------------------------------------------------------------*/
static void
muse_resampling_override_size_int(int *aV, const char *aKeyword, int aValue)
{
  if (aValue <= 0 || !aV) {
    return;
  }
  const char func[] = "muse_resampling_cube"; /* pretend to be in that fct... */
  cpl_msg_info(func, "Overriding %s=%d (was %d)", aKeyword, aValue, *aV);
  *aV = aValue;
} /* muse_resampling_override_size_int() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Override cube sizes (using NAXIS).
  @param   aX        pointer to horizontal cube size to update
  @param   aY        pointer to vertical cube size to update
  @param   aZ        pointer to dispersion direction cube size to update
  @param   aParams   the resampling parameters which may carry the WCS
  @return  CPL_ERROR_NONE on non-critical failure or success, another CPL error
           code otherwise.
  @error{return CPL_ERROR_NULL_INPUT, aX\, aY\, aZ\, or aParams are NULL}
  @error{return CPL_ERROR_NONE, aParams->wcs is NULL}
  @error{return CPL_ERROR_NONE, aParams->wcs does not contain dimension info}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_override_size(int *aX, int *aY, int *aZ,
                              muse_resampling_params *aParams)
{
  cpl_ensure_code(aX && aY && aZ && aParams, CPL_ERROR_NULL_INPUT);
  if (!aParams->wcs) { /* quietly return */
    return CPL_ERROR_NONE;
  }
  const char func[] = "muse_resampling_cube"; /* pretend to be in that fct... */
  /* cube size overrides */
  const cpl_array *dims = cpl_wcs_get_image_dims(aParams->wcs);
  if (!dims) {
    cpl_msg_debug(func, "No dimensions to override were specified");
    return CPL_ERROR_NONE;
  }
  muse_resampling_override_size_int(aX, "NAXIS1", cpl_array_get_int(dims, 0, NULL));
  muse_resampling_override_size_int(aY, "NAXIS2", cpl_array_get_int(dims, 1, NULL));
  if (cpl_wcs_get_image_naxis(aParams->wcs) >= 3) {
    muse_resampling_override_size_int(aZ, "NAXIS3",
                                      cpl_array_get_int(dims, 2, NULL));
  }
  return CPL_ERROR_NONE;
} /* muse_resampling_override_size() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Override double WCS property in cube header and output message.
  @param   aCube      cube the header of which gets updated
  @param   aKeyword   FITS keyword name to output
  @param   aValue     the value to replace with
  @param   aError     if an error occurred accessing the keyword
  @error{return, aError is not zero}
  @error{return, aCube is NULL}
 */
/*---------------------------------------------------------------------------*/
static void
muse_resampling_override_wcs_double(muse_datacube *aCube, const char *aKeyword,
                                    double aValue, int aError)
{
  if (aError || !aCube) {
    cpl_msg_debug("double", "%s=%#g (%d)", aKeyword, aValue, aError);
    return;
  }
  const char func[] = "muse_resampling_cube"; /* pretend to be in that fct... */
  double old = cpl_propertylist_has(aCube->header, aKeyword)
             ? cpl_propertylist_get_double(aCube->header, aKeyword) : NAN;
  cpl_msg_info(func, "Overriding %s=%#g (was %#g)", aKeyword, aValue, old);
  cpl_propertylist_update_double(aCube->header, aKeyword, aValue);
  /* Leave the marked that something was overridden, will *
   * be evaluated and removed in muse_pixgrid_create()!   */
  cpl_propertylist_update_bool(aCube->header, "MUSE_RESAMPLING_WCS_OVERRIDDEN",
                               CPL_TRUE);
} /* muse_resampling_override_wcs_double() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Override WCS properties (except NAXIS) in cube header.
  @param   aCube     cube the header of which gets updated
  @param   aParams   the resampling parameters which may carry the WCS
  @return  CPL_ERROR_NONE on non-critical failure or success, another CPL error
           code otherwise.
  @error{return CPL_ERROR_NULL_INPUT,
         aCube\, its header component\, or aParams are NULL}
  @error{return CPL_ERROR_NONE, aParams->wcs is NULL}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_override_wcs(muse_datacube *aCube,
                             muse_resampling_params *aParams)
{
  cpl_ensure_code(aCube && aCube->header && aParams, CPL_ERROR_NULL_INPUT);
  if (!aParams->wcs) { /* quietly return */
    return CPL_ERROR_NONE;
  }
  const char func[] = "muse_resampling_cube"; /* pretend to be in that fct... */
  const cpl_array *crval = cpl_wcs_get_crval(aParams->wcs),
                  *crpix = cpl_wcs_get_crpix(aParams->wcs);
  const cpl_matrix *cd = cpl_wcs_get_cd(aParams->wcs);
  int err = 0;
  /* spatial axes overrides */
  if (crval) {
    muse_resampling_override_wcs_double(aCube, "CRVAL1", cpl_array_get_double(crval, 0, &err), err);
    muse_resampling_override_wcs_double(aCube, "CRVAL2", cpl_array_get_double(crval, 1, &err), err);
  } else {
    cpl_msg_debug(func, "No CRVALj to override were specified");
  }
  if (crpix) {
    muse_resampling_override_wcs_double(aCube, "CRPIX1", cpl_array_get_double(crpix, 0, &err), err);
    muse_resampling_override_wcs_double(aCube, "CRPIX2", cpl_array_get_double(crpix, 1, &err), err);
  } else {
    cpl_msg_debug(func, "No CRPIXi to override were specified");
  }
  if (cd) {
    int naxes = cpl_matrix_get_ncol(cd); /* nrow should be the same */
    if (cpl_matrix_get_determinant(cd) == 0.) {
      cpl_msg_warning(func, "det(CDi_j) = 0, not overriding CDi_j!");
      cd = NULL; /* don't override dispersion direction, either */
    } else if (naxes > 2 && (cpl_matrix_get(cd, 0, 2) != 0. ||
                             cpl_matrix_get(cd, 2, 0) != 0. ||
                             cpl_matrix_get(cd, 2, 1) != 0. ||
                             cpl_matrix_get(cd, 1, 2) != 0.)) {
      cpl_msg_warning(func, "Axis 3 (dispersion) is not cleanly separated from "
                      "axes 1 and 2, not overriding CDi_j!");
      cd = NULL; /* don't override dispersion direction, either */
    } else {
      cpl_errorstate es = cpl_errorstate_get();
      muse_resampling_override_wcs_double(aCube, "CD1_1", cpl_matrix_get(cd, 0, 0),
                                          !cpl_errorstate_is_equal(es));
      es = cpl_errorstate_get();
      muse_resampling_override_wcs_double(aCube, "CD1_2", cpl_matrix_get(cd, 0, 1),
                                          !cpl_errorstate_is_equal(es));
      es = cpl_errorstate_get();
      muse_resampling_override_wcs_double(aCube, "CD2_1", cpl_matrix_get(cd, 1, 0),
                                          !cpl_errorstate_is_equal(es));
      es = cpl_errorstate_get();
      muse_resampling_override_wcs_double(aCube, "CD2_2", cpl_matrix_get(cd, 1, 1),
                                          !cpl_errorstate_is_equal(es));
    }
  } else {
    cpl_msg_debug(func, "No CDi_j to override were specified");
  }
  /* wavelength axis overrides; apparently, values originally in Angstrom  *
   * values are here in meters (no way to check this here, since we cannot *
   * access aParams->wcs->wcsptr!), so need to convert them back           */
  if (cpl_array_get_size(crval) > 2) {
    if (crval) {
      muse_resampling_override_wcs_double(aCube, "CRVAL3",
                                          cpl_array_get_double(crval, 2, &err) * 1e10, err);
    }
    if (crpix) {
      muse_resampling_override_wcs_double(aCube, "CRPIX3", cpl_array_get_double(crpix, 2, &err), err);
    }
    if (cd) {
      cpl_errorstate es = cpl_errorstate_get();
      muse_resampling_override_wcs_double(aCube, "CD3_3", cpl_matrix_get(cd, 2, 2) * 1e10,
                                          !cpl_errorstate_is_equal(es));
    }
  } /* if 3D */
  return CPL_ERROR_NONE;
} /* muse_resampling_override_wcs() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Update CRPIX[12] so that the data fits into the output grid.
  @param   aCube       the output datacube (its header component is used here)
  @param   aPixtable   the relevant pixel table (its header component is used)
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  @error{set and return CPL_ERROR_NULL_INPUT, aCube or aPixtable are NULL}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_fit_data_to_grid(muse_datacube *aCube, muse_pixtable *aPixtable)
{
  cpl_ensure_code(aCube && aPixtable, CPL_ERROR_NULL_INPUT);
  const char func[] = "muse_resampling_cube"; /* pretend to be in that fct... */
  /* Check if we need to move the spatial zeropoint to fit into the *
   * computed grid at all. This is only the case, if the grid was   *
   * computed automatically, not overridden.                        */
  if (cpl_propertylist_has(aCube->header, "MUSE_RESAMPLING_WCS_OVERRIDDEN") &&
      cpl_propertylist_get_bool(aCube->header, "MUSE_RESAMPLING_WCS_OVERRIDDEN")) {
    cpl_msg_debug(func, "Output WCS was forced, won't update CRPIX[12]!");
    cpl_propertylist_erase(aCube->header, "MUSE_RESAMPLING_WCS_OVERRIDDEN");
    return CPL_ERROR_NONE;
  }
  /* do determine offset of coordinate to the first (1,1) pixel */
  double xoff1, yoff1, xoff2, yoff2;
  float xlo = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XLO),
        xhi = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_XHI),
        ylo = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YLO),
        yhi = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_YHI);
  muse_pixtable_wcs wcstype = muse_pixtable_wcs_check(aPixtable);
  if (wcstype == MUSE_PIXTABLE_WCS_CELSPH) {
    muse_wcs_pixel_from_celestial(aCube->header, xlo, ylo, &xoff1, &yoff1);
    muse_wcs_pixel_from_celestial(aCube->header, xhi, yhi, &xoff2, &yoff2);
  } else {
    muse_wcs_pixel_from_projplane(aCube->header, xlo, ylo, &xoff1, &yoff1);
    muse_wcs_pixel_from_projplane(aCube->header, xhi, yhi, &xoff2, &yoff2);
  }
  /* the actual minimum offset we need depends on the axis-direction,  *
   * i.e. the sign of CDi_j, so we take the minimum of the result here */
  double xoff = fmin(xoff1, xoff2) - 1,
         yoff = fmin(yoff1, yoff2) - 1;
  if (xoff != 0. || yoff != 0.) {
    double crpix1 = muse_pfits_get_crpix(aCube->header, 1),
           crpix2 = muse_pfits_get_crpix(aCube->header, 2);
    cpl_msg_debug(func, "Updating CRPIX[12]=%#g,%#g (were %#g,%#g)",
                  crpix1 - xoff, crpix2 - yoff, crpix1, crpix2);
    crpix1 -= xoff;
    crpix2 -= yoff;
    cpl_propertylist_update_double(aCube->header, "CRPIX1", crpix1);
    cpl_propertylist_update_double(aCube->header, "CRPIX2", crpix2);
  } /* if offsets */
  return CPL_ERROR_NONE;
} /* muse_resampling_fit_data_to_grid() */

/* corresponds to muse_resampling_crstats_type, *
 * keep in sync with those values!              */
const char *crtypestring[] = {
  "IRAF-like",
  "average",
  "median"
};

#define MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_START                                 \
              cpl_size idx = muse_pixgrid_get_index(aGrid, i2, j2, l2, CPL_FALSE),   \
                       nrow = muse_pixgrid_get_count(aGrid, idx),                    \
                       irow;                                                         \
              const cpl_size *rows = muse_pixgrid_get_rows(aGrid, idx);              \
              for (irow = 0; irow < nrow; irow++) {                                  \
                if (muse_quality_is_usable(dq[rows[irow]], badinclude)) {            \
                  /* do nothing */                                                   \
                } else if (dq[rows[irow]]) {                                         \
                  continue; /* exclude all other bad pixels */                       \
                }
#define MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_DEBUG                                 \
                if (debug & 4 && i+1 == debugx && j+1 == debugy && l+1 == debugz) {  \
                  printf("%s: data / stat (%"CPL_SIZE_FORMAT") = %e / %e\n",         \
                         __func__, npixels+1, data[rows[irow]], stat[rows[irow]]);   \
                }
#define MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_END                                   \
                if (aType == MUSE_RESAMPLING_CRSTATS_IRAF) {                         \
                  /* add the point from the pixel table to the vector */             \
                  level += data[rows[irow]];                                         \
                  dev += stat[rows[irow]];                                           \
                } else {                                                             \
                  /* add the point to the npixels'th column of the image */          \
                  if (npixels >= sxsize) {                                           \
                    /* is there no cpl_image_resize()?! */                           \
                    sxsize += MUSE_CRREJECT_MAX_NPIX;                                \
                    cpl_image *tmp = cpl_image_new(sxsize, 2, CPL_TYPE_DOUBLE);      \
                    cpl_image_copy(tmp, sdata, 1, 1);                                \
                    cpl_image_delete(sdata);                                         \
                    sdata = tmp;                                                     \
                    vdata = cpl_image_get_data_double(sdata);                        \
                  }                                                                  \
                  vdata[npixels] = data[rows[irow]];                                 \
                  vdata[npixels + sxsize] = stat[rows[irow]];                        \
                }                                                                    \
                /* increase the index for all methods */                             \
                npixels++;                                                           \
              } /* for irow (all pixels in grid cell) */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Reject cosmic rays in the pixel grid.
  @param   aPixtable   the input pixel table
  @param   aGrid       the input pixel grid
  @param   aType       statistics type used for detection
  @param   aSigma      sigma level for cosmic ray rejection

  This loops through surrounding pixels of each output grid cell and marks
  those which are higher than a limit (defined using the aSigma parameter) to
  be cosmic rays.
  The trick here is to only compare data that are not adjacent on the CCD, i.e.
  data coming from different slices. This is the case for data that have a
  vertical offset (in the y coordinate) from the target pixel.

  Depending on aType, the statistics are computed differently (IRAF-like, using
  average/stdev, or median/mad), so that the interpretation of the aSigma
  parameter changes, too.

  @error{set CPL_ERROR_ILLEGAL_INPUT and error message\, use MUSE_RESAMPLING_CRSTATS_IRAF,
         an unknown aType is given}
 */
/*---------------------------------------------------------------------------*/
static void
muse_resampling_crreject(muse_pixtable *aPixtable, muse_pixgrid *aGrid,
                         muse_resampling_crstats_type aType, double aSigma)
{
  const char *id = "muse_resampling_cube"; /* pretend to be in that function */
  if (aType > MUSE_RESAMPLING_CRSTATS_MEDIAN) {
    cpl_msg_warning(id, "Unknown type (%u) for cosmic-ray rejection statistics,"
                    " resetting to \"%s\"", aType,
                    crtypestring[MUSE_RESAMPLING_CRSTATS_MEDIAN]);
    aType = MUSE_RESAMPLING_CRSTATS_MEDIAN;
  }
  cpl_msg_info(id, "Using %s statistics (%.3f sigma level) for cosmic-ray"
               " rejection", crtypestring[aType], aSigma);

#ifdef ESO_ENABLE_DEBUG
  int debug = 0, debugx = 0, debugy = 0, debugz = 0;
  if (getenv("MUSE_DEBUG_CRREJECT")) {
    debug = atoi(getenv("MUSE_DEBUG_CRREJECT"));
  }
  if (debug & 2) { /* need coordinates */
    if (getenv("MUSE_DEBUG_CRREJECT_X")) {
      debugx = atoi(getenv("MUSE_DEBUG_CRREJECT_X"));
      if (debugx < 1 || debugx > aGrid->nx) {
        debugx = 0;
      }
    }
    if (getenv("MUSE_DEBUG_CRREJECT_Y")) {
      debugy = atoi(getenv("MUSE_DEBUG_CRREJECT_Y"));
      if (debugy < 1 || debugy > aGrid->ny) {
        debugy = 0;
      }
    }
    if (getenv("MUSE_DEBUG_CRREJECT_Z")) {
      debugz = atoi(getenv("MUSE_DEBUG_CRREJECT_Z"));
      if (debugz < 1 || debugz > aGrid->nz) {
        debugz = 0;
      }
    }
  }
#endif

  enum dirtype {
    DIR_X = 0,
    DIR_Y = 1,
    DIR_NONE = 2
  };
  enum dirtype dir = DIR_NONE;
  /* XXX also exclude multiple exposures with different POSANG! */
  cpl_boolean haswcs = muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH;
  double posang = muse_astro_posangle(aPixtable->header);
  const double palimit = 5.;
  if (!haswcs || (fabs(posang) < palimit || fabs(fabs(posang) - 180.) < palimit ||
                  fabs(fabs(posang) - 360.) < palimit)) {
    cpl_msg_debug(id, "CR rejection: posang = %f deg --> DIR_X "
                  "(%s / %s / %s / %s)", posang, haswcs ? "yes": "no",
                  fabs(posang) < palimit ? "true" : "false",
                  fabs(fabs(posang) - 180.) < palimit ? "true" : "false",
                  fabs(fabs(posang) - 360.) < palimit ? "true" : "false");
    dir = DIR_X;
  } else if (fabs(fabs(posang) - 90.) < palimit ||
             fabs(fabs(posang) - 270.) < palimit) {
    cpl_msg_debug(id, "CR rejection: posang = %f deg --> DIR_Y "
                  "(%s / %s / %s)", posang, haswcs ? "yes": "no",
                  fabs(fabs(posang) - 90.) < palimit ? "true" : "false",
                  fabs(fabs(posang) - 270.) < palimit ? "true" : "false");
    dir = DIR_Y;
  } else {
    cpl_msg_debug(id, "CR rejection: posang = %f deg --> DIR_NONE "
                  "(%s / %s / %s / %s / %s / %s)", posang, haswcs ? "yes": "no",
                  fabs(posang) < palimit ? "true" : "false",
                  fabs(fabs(posang) - 90.) < palimit ? "true" : "false",
                  fabs(fabs(posang) - 180.) < palimit ? "true" : "false",
                  fabs(fabs(posang) - 270.) < palimit ? "true" : "false",
                  fabs(fabs(posang) - 360.) < palimit ? "true" : "false");
  }

  /* pointer access to the relevant pixel table columns */
  float *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA),
        *stat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT);
  int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);

  /* when computing surrounding statistics, include these types of bad pixels */
  uint32_t badinclude = EURO3D_COSMICRAY;
  int l;
#ifdef ESO_ENABLE_DEBUG
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aGrid, aPixtable, aSigma, badinclude, aType, crtypestring,    \
                 data, debug, debugx, debugy, debugz, dir, dq, id, stat, stdout)
#else
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aGrid, aSigma, aType, badinclude, crtypestring, data, dir,    \
                 dq, id, stat)
#endif
  for (l = 0; l < aGrid->nz; l++) {
#define MUSE_CRREJECT_MAX_NPIX 1000 /* XXX compute size dynamically somehow */
    int sxsize = MUSE_CRREJECT_MAX_NPIX;
    cpl_image *sdata = NULL;
    double *vdata = NULL;
    if (aType > MUSE_RESAMPLING_CRSTATS_IRAF) {
      /* create image for statistics computations */
      /* XXX would memset'ting the buffer not be faster? */
      sdata = cpl_image_new(sxsize, 2, CPL_TYPE_DOUBLE);
      vdata = cpl_image_get_data_double(sdata);
    }

    int i;
    for (i = 0; i < aGrid->nx; i++) {
      int j;
      for (j = 0; j < aGrid->ny; j++) {
        /* in the IRAF-like case, compute local statistics, i.e. set *
         * the variables to zero (mean, and stdev from the variance) */
        double level = 0., dev = 0;
        /* loop through surrounding cells: compute local statistics */
#define CR_LD 1 /* loop distance for CR rejection statistics */
        cpl_size npixels = 0; /* count pixels again, without bad ones */
        int i2, j2, l2;
        if (dir == DIR_Y) {
          for (i2 = i - CR_LD; i2 <= i + CR_LD; i2++) {
            int nwidth = CR_LD;
            if (i2 == i) {
              nwidth = 0; /* don't use neighbors in the same slice */
            }
            for (j2 = j - nwidth; j2 <= j + nwidth; j2++) {
              for (l2 = l - nwidth; l2 <= l + nwidth; l2++) {
                MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_START
#ifdef ESO_ENABLE_DEBUG
                MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_DEBUG
#endif
                MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_END
              } /* for l2 (wavelength/z direction) */
            } /* for i2 (x direction) */
          } /* for j2 (y direction) */
        } else {
          for (j2 = j - CR_LD; j2 <= j + CR_LD; j2++) {
            int nwidth = CR_LD;
            if (dir == DIR_X && j2 == j) {
              nwidth = 0; /* don't use neighbors in the same slice */
            }
            for (i2 = i - nwidth; i2 <= i + nwidth; i2++) {
              for (l2 = l - nwidth; l2 <= l + nwidth; l2++) {
                MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_START
#ifdef ESO_ENABLE_DEBUG
                MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_DEBUG
#endif
                MUSE_RESAMPLING_CRREJECT_COMPUTE_STATS_END
              } /* for l2 (wavelength/z direction) */
            } /* for i2 (x direction) */
          } /* for j2 (y direction) */
        } /* else */
        /* need at least 3 values to do something sensible */
        if (npixels < 3) {
          continue;
        }
        if (aType == MUSE_RESAMPLING_CRSTATS_IRAF) {
          level /= npixels; /* local mean value */
          dev = sqrt(dev) / npixels; /* local mean sigma value */
        } else {
          unsigned sflags;
          if (aType == MUSE_RESAMPLING_CRSTATS_MEDIAN) {
            sflags = CPL_STATS_MEDIAN | CPL_STATS_MAD;
          } else {
            sflags = CPL_STATS_MEAN | CPL_STATS_STDEV;
          }
          cpl_stats *sstats = cpl_stats_new_from_image_window(sdata, sflags,
                                                              1, 1, npixels, 1);
          if (aType == MUSE_RESAMPLING_CRSTATS_MEDIAN) {
            level = cpl_stats_get_median(sstats); /* local median value */
            dev = cpl_stats_get_mad(sstats); /* local MAD */
          } else {
            level = cpl_stats_get_mean(sstats); /* local mean value */
            dev = cpl_stats_get_stdev(sstats); /* local standard deviation */
          }
          cpl_stats_delete(sstats);
        } /* else (not IRAF) */
        double limit = level + aSigma * dev;
#ifdef ESO_ENABLE_DEBUG
        if (debug & 2 && i+1 == debugx && j+1 == debugy && l+1 == debugz) {
          if (aType == MUSE_RESAMPLING_CRSTATS_IRAF) {
            printf("%s: %03d,%03d,%04d: %.3f+/-%.3f (stats), %.3f (limit) (%"
                   CPL_SIZE_FORMAT" values)\n", __func__, i+1, j+1, l+1, level,
                   dev, limit, npixels);
          } else {
            cpl_stats *ssdata = cpl_stats_new_from_image_window(sdata, CPL_STATS_ALL,
                                                                1, 1, npixels, 1),
                      *ssstat = cpl_stats_new_from_image_window(sdata, CPL_STATS_ALL,
                                                                1, 2, npixels, 2);
            printf("%s: %03d,%03d,%04d: %e +/- %e (%s), %e (limit) (%"CPL_SIZE_FORMAT
                   " values); data stats:\n", __func__,  i+1, j+1, l+1,
                   level, dev, crtypestring[aType], limit, npixels);
            cpl_stats_dump(ssdata, CPL_STATS_ALL, stdout);
            printf("%s: variance stats:\n", __func__);
            cpl_stats_dump(ssstat, CPL_STATS_ALL, stdout);
            fflush(stdout);
            cpl_stats_delete(ssdata);
            cpl_stats_delete(ssstat);
          }
        }
#endif

        /* loop through pixels in the central cell: mark cosmic rays */
        cpl_size idx = muse_pixgrid_get_index(aGrid, i, j, l, CPL_FALSE),
                 nrow = muse_pixgrid_get_count(aGrid, idx),
                 irow;
        const cpl_size *rows = muse_pixgrid_get_rows(aGrid, idx);
        for (irow = 0; irow < nrow; irow++) {
          if (data[rows[irow]] > limit) {
            dq[rows[irow]] |= EURO3D_COSMICRAY;
#ifdef ESO_ENABLE_DEBUG
            if (debug & 1 && i+1 == debugx && j+1 == debugy && l+1 == debugz) {
              printf("%s: %03d,%03d,%04d: rejected row %"CPL_SIZE_FORMAT" (%"
                     CPL_SIZE_FORMAT" of %"CPL_SIZE_FORMAT" in this gridcell):\t",
                     __func__, i+1, j+1, l+1, rows[irow], irow+1, nrow);
              muse_pixtable_dump(aPixtable, rows[irow], 1, 0);
            }
#endif
          }
        } /* for irow (all pixels in grid cell) */
#ifdef ESO_ENABLE_DEBUG
        if (debug) {
          fflush(stdout);
        }
#endif
      } /* for j (y direction) */
    } /* for i (x direction) */
    cpl_image_delete(sdata);
  } /* for l (wavelength planes) */
} /* muse_resampling_crreject() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Resample a pixel table onto a regular grid structure representing
           a Euro3D format file.
  @param   aPixtable    the MUSE pixel table to resample
  @param   aParams      the structure of resampling parameters
  @return  a muse_euro3dcube * for the output Euro3D data or NULL on error

  See @c muse_resampling_cube() for the description of the algorithm. This
  function uses that output cube and converts it into a Euro3D table structure.

  This function decides from the FITS keywords available in the pixel table
  header if the data was corrected for DAR and which reference wavelength was
  used, see muse_dar_correct().

  @qa See muse_resampling_cube().

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, aParams is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aParams->method is not a real resampling type}
  @error{return NULL\, propagate error code of muse_resampling_cube(),
         resampling fails}
 */
/*---------------------------------------------------------------------------*/
muse_euro3dcube *
muse_resampling_euro3d(muse_pixtable *aPixtable,
                       muse_resampling_params *aParams)
{
  cpl_ensure(aParams, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aParams->method < MUSE_RESAMPLE_NONE, CPL_ERROR_ILLEGAL_INPUT, NULL);
  /* all other error checking is already done in muse_resampling_cube() */

  /* call the existing resampling routine for datacubes */
  muse_datacube *cube = muse_resampling_cube(aPixtable, aParams, NULL);
  if (!cube) {
    return NULL;
  }

  /* create Euro3D object, copy header and set typical Euro3D primary headers */
  muse_euro3dcube *e3d = cpl_calloc(1, sizeof(muse_euro3dcube));
  e3d->header = cpl_propertylist_duplicate(cube->header);
  cpl_propertylist_erase_regexp(e3d->header,
                                "^SIMPLE$|^BITPIX$|^NAXIS|^EURO3D$|^E3D_",
                                0);
  cpl_propertylist_append_char(e3d->header, "EURO3D", 'T');
  cpl_propertylist_set_comment(e3d->header, "EURO3D",
                               "file conforms to Euro3D standard");
  /* E3D_VERS is supposed to be a string! */
  cpl_propertylist_append_string(e3d->header, "E3D_VERS", "1.0");
  cpl_propertylist_set_comment(e3d->header, "E3D_VERS",
                               "version number of the Euro3D format");
  cpl_errorstate prestate = cpl_errorstate_get();
  double darlambdaref = cpl_propertylist_get_double(e3d->header,
                                                    MUSE_HDR_PT_DAR_NAME);
  if (!cpl_errorstate_is_equal(prestate)) {
    darlambdaref = -1.; /* set to negative value to be sure on error */
    cpl_errorstate_set(prestate);
  }
  if (darlambdaref > 0.) {
    cpl_propertylist_append_char(e3d->header, "E3D_ADC", 'T');
    cpl_propertylist_set_comment(e3d->header, "E3D_ADC",
                                 "data was corrected for atmospheric dispersion");
  } else {
    cpl_propertylist_append_char(e3d->header, "E3D_ADC", 'F');
    cpl_propertylist_set_comment(e3d->header, "E3D_ADC",
                                 "data not corrected for atmospheric dispersion");
  }

  /* fill WCS info in data header */
  e3d->hdata = cpl_propertylist_new();
  cpl_propertylist_append_string(e3d->hdata, "EXTNAME", "E3D_DATA");
  cpl_propertylist_set_comment(e3d->hdata, "EXTNAME",
                               "This is the Euro3D data table extension");
  cpl_propertylist_append_string(e3d->hdata, "CTYPES",
                                 muse_pfits_get_ctype(e3d->header, 3));
  cpl_propertylist_set_comment(e3d->hdata, "CTYPES",
                               cpl_propertylist_get_comment(e3d->header, "CTYPE3"));
  cpl_propertylist_append_string(e3d->hdata, "CUNITS",
                                 muse_pfits_get_cunit(e3d->header, 3));
  cpl_propertylist_set_comment(e3d->hdata, "CUNITS",
                               cpl_propertylist_get_comment(e3d->header, "CUNIT3"));
  cpl_propertylist_append_double(e3d->hdata, "CRVALS",
                                 muse_pfits_get_crval(e3d->header, 3));
  cpl_propertylist_set_comment(e3d->hdata, "CRVALS",
                               cpl_propertylist_get_comment(e3d->header, "CRVAL3"));
  cpl_propertylist_append_double(e3d->hdata, "CDELTS",
                                 muse_pfits_get_cd(e3d->header, 3, 3));
  cpl_propertylist_set_comment(e3d->hdata, "CDELTS",
                               cpl_propertylist_get_comment(e3d->header, "CD3_3"));
  /* remove WCS for spatial axis from main header */
  cpl_propertylist_erase_regexp(e3d->header, "^C...*3$|^CD3_.$", 0);

  /* create and fill Euro3D table structure */
  int nx = cpl_image_get_size_x(cpl_imagelist_get(cube->data, 0)),
      ny = cpl_image_get_size_y(cpl_imagelist_get(cube->data, 0)),
      nz = cpl_imagelist_get_size(cube->data);
  e3d->dtable = muse_cpltable_new(muse_euro3dcube_e3d_data_def, nx * ny);
  /* set array columns to initial (expected) depth */
  /* XXX this is very expensive for big tables, we should make    *
   *     muse_cpltable_new() take an extra argument for the depth */
  cpl_table_set_column_depth(e3d->dtable, "DATA_SPE", nz);
  cpl_table_set_column_depth(e3d->dtable, "QUAL_SPE", nz);
  cpl_table_set_column_depth(e3d->dtable, "STAT_SPE", nz);
  /* set column units depending on data unit type in the pixel table */
  cpl_table_set_column_unit(e3d->dtable, "DATA_SPE",
                            cpl_table_get_column_unit(aPixtable->table,
                                                      MUSE_PIXTABLE_DATA));
  cpl_table_set_column_unit(e3d->dtable, "STAT_SPE",
                            cpl_table_get_column_unit(aPixtable->table,
                                                      MUSE_PIXTABLE_STAT));
  /* set column save types as needed */
  cpl_table_set_column_savetype(e3d->dtable, "SELECTED", CPL_TYPE_BOOL);

  muse_wcs *wcs = muse_wcs_new(cube->header);
  wcs->iscelsph = muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH;
  if (wcs->iscelsph) { /* if not, then the preset unit of "pix" is valid */
    cpl_table_set_column_unit(e3d->dtable, "XPOS", "deg");
    cpl_table_set_column_unit(e3d->dtable, "YPOS", "deg");
  }

  /* Loop over all spaxels in the cube and transfer the data */
  unsigned euro3d_ignore = EURO3D_OUTSDRANGE | EURO3D_MISSDATA;
  cpl_vector *vusable = cpl_vector_new(nx * ny); /* for statistics */
  int i, itable = 0;
  for (i = 1; i <= nx; i++) {
    int j;
    for (j = 1; j <= ny; j++) {
      cpl_array *adata = cpl_array_new(nz, CPL_TYPE_FLOAT),
                *adq = cpl_array_new(nz, CPL_TYPE_INT),
                *astat = cpl_array_new(nz, CPL_TYPE_FLOAT);
      /* WCS coordinates */
      double x, y;
      if (wcs->iscelsph) {
        muse_wcs_celestial_from_pixel_fast(wcs, i, j, &x, &y);
      } else {
        muse_wcs_projplane_from_pixel_fast(wcs, i, j, &x, &y);
      }

      int l, nusable = 0, nstart = -1; /* numbers needed for Euro3D format */
      for (l = 0; l < nz; l++) {
        int err;
        unsigned dq = cpl_image_get(cpl_imagelist_get(cube->dq, l), i, j, &err);
        /* check if we can ignore this as bad pixel at the start */
        if (nstart < 0 && (dq & euro3d_ignore)) {
          continue;
        }
        cpl_array_set_int(adq, nusable, dq);
        cpl_array_set_float(adata, nusable,
                            cpl_image_get(cpl_imagelist_get(cube->data, l),
                                          i, j, &err));
        /* take the square root so that we get errors and not variances */
        cpl_array_set_float(astat, nusable,
                            sqrt(cpl_image_get(cpl_imagelist_get(cube->stat, l),
                                               i, j, &err)));
        nusable++;
        if (nstart < 0) {
          nstart = l + 1;
        }
      } /* for l (z / wavelengths) */

      cpl_table_set_int(e3d->dtable, "SPEC_ID", itable, itable + 1);
      cpl_table_set_int(e3d->dtable, "SPEC_LEN", itable, nusable);
      cpl_table_set_int(e3d->dtable, "SPEC_STA", itable, nstart);
      cpl_table_set_double(e3d->dtable, "XPOS", itable, x);
      cpl_table_set_double(e3d->dtable, "YPOS", itable, y);
      /* not sure what to use for SPAX_ID, just leave it unset for now */
      /*cpl_table_set_string(e3d->dtable, "SPAX_ID", itable, ""); */
      cpl_table_set_array(e3d->dtable, "DATA_SPE", itable, adata);
      cpl_table_set_array(e3d->dtable, "QUAL_SPE", itable, adq);
      cpl_table_set_array(e3d->dtable, "STAT_SPE", itable, astat);

      cpl_array_delete(adata);
      cpl_array_delete(adq);
      cpl_array_delete(astat);

      cpl_vector_set(vusable, itable, nusable);
      if (nstart != -1 && nusable > 0) {
        /* good spectrum, unselect to not remove it below */
        cpl_table_unselect_row(e3d->dtable, itable);
      }
      itable++; /* move to next table row */
    } /* for j (y spaxels) */
  } /* for i (x spaxels) */
  cpl_free(wcs);

  cpl_vector_set_size(vusable, itable);
  int maxusable = cpl_vector_get_max(vusable);
#if 0
  cpl_msg_debug(__func__, "filled %d of %d spaxels, usable are "
                "%f..%f(%f)+/-%f..%d pixels per spectrum", itable, nx * ny,
                cpl_vector_get_min(vusable), cpl_vector_get_mean(vusable),
                cpl_vector_get_median(vusable), cpl_vector_get_stdev(vusable),
                maxusable);
#endif
  cpl_msg_debug(__func__, "filled %"CPL_SIZE_FORMAT" of %d spaxels, usable "
                "are max. %d of %d pixels per spectrum%s",
                itable - cpl_table_count_selected(e3d->dtable), nx * ny,
                maxusable, nz, maxusable == nz ? "" : ", cutting table");
  if (maxusable != nz) {
    /* set array columns to real depth (maximum usable spectral pixels found) */
    cpl_table_set_column_depth(e3d->dtable, "DATA_SPE", maxusable);
    cpl_table_set_column_depth(e3d->dtable, "QUAL_SPE", maxusable);
    cpl_table_set_column_depth(e3d->dtable, "STAT_SPE", maxusable);
  }
  /* resize table to number of used spaxels */
  cpl_table_erase_selected(e3d->dtable); /* erase empty spectra */
  cpl_vector_delete(vusable);

  /* fill columns that are the same for all spaxels */
  /* by default, all spectra are supposed to be selected */
  cpl_table_fill_column_window_int(e3d->dtable, "SELECTED", 0, itable,
                                   1 /* TRUE */);
  /* assume that we only used one resolution element */
  cpl_table_fill_column_window_int(e3d->dtable, "NSPAX", 0, itable, 1);
  /* we have only one group for MUSE */
  cpl_table_fill_column_window_int(e3d->dtable, "GROUP_N", 0, itable, 1);

  /* done converting the data, free the datacube  */
  muse_datacube_delete(cube);

  /* fill info in group table */
  e3d->hgroup = cpl_propertylist_new();
  cpl_propertylist_append_string(e3d->hgroup, "EXTNAME", "E3D_GRP");
  cpl_propertylist_set_comment(e3d->hgroup, "EXTNAME",
                               "This is the Euro3D group table extension");
  e3d->gtable = muse_cpltable_new(muse_euro3dcube_e3d_grp_def, 1); /* single group */
  cpl_table_set_int(e3d->gtable, "GROUP_N", 0, 1);
  cpl_table_set_string(e3d->gtable, "G_SHAPE", 0, "R"/*ECTANG" ULAR*/);
  cpl_table_set_float(e3d->gtable, "G_SIZE1", 0, aParams->dx);
  cpl_table_set_float(e3d->gtable, "G_ANGLE", 0, 0.);
  cpl_table_set_float(e3d->gtable, "G_SIZE2", 0, aParams->dy);
  if (darlambdaref > 0.) {
    /* set all properties to NaN as required by the Euro3D specs */
    cpl_table_set_float(e3d->gtable, "G_POSWAV", 0, NAN);
    cpl_table_set_float(e3d->gtable, "G_AIRMAS", 0, NAN);
    cpl_table_set_float(e3d->gtable, "G_PARANG", 0, NAN);
    cpl_table_set_float(e3d->gtable, "G_PRESSU", 0, NAN);
    cpl_table_set_float(e3d->gtable, "G_TEMPER", 0, NAN);
    cpl_table_set_float(e3d->gtable, "G_HUMID", 0, NAN);
  } else {
    /* because we then don't know any better, use the central MUSE wavelength */
    cpl_table_set_float(e3d->gtable, "G_POSWAV", 0,
                        (kMuseNominalLambdaMin + kMuseNominalLambdaMax) / 2.);
    cpl_table_set_float(e3d->gtable, "G_AIRMAS", 0,
                        muse_astro_airmass(e3d->header));
    cpl_table_set_float(e3d->gtable, "G_PARANG", 0,
                        muse_astro_parangle(e3d->header));
    cpl_table_set_float(e3d->gtable, "G_PRESSU", 0,
                        (muse_pfits_get_pres_start(e3d->header)
                         + muse_pfits_get_pres_start(e3d->header)) / 2.);
    cpl_table_set_float(e3d->gtable, "G_TEMPER", 0,
                          muse_pfits_get_temp(e3d->header) + 273.15);
    cpl_table_set_float(e3d->gtable, "G_HUMID", 0,
                        muse_pfits_get_rhum(e3d->header));
  }

  return e3d;
} /* muse_resampling_euro3d() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Resample a pixel table onto a regular grid structure representing
           a FITS NAXIS=3 datacube.
  @param   aPixtable   the MUSE pixel table to resample
  @param   aParams     the structure of resampling parameters
  @param   aGrid       if not NULL, use it to store the pixel grid pointer
  @return  A muse_datacube * for the output FITS datacube (and its bad pixels,
           variance and headers) or NULL on error.

  This function implements the resampling scheme discussed in Sect. 2.2 of the
  DRLDesign document:
  First, convert the input pixel table into a regular grid of cells, storing
  the input pixels in their nearest cell. To resample, then loop through all
  cells, sampling surrounding pixels depending on the requested method. Values
  that rise aHSigma above the surrounding values, i.e. cosmic rays, are removed
  at this stage. Store the output pixels (their values, bad pixel status, and
  variance) in a muse_datacube structure to be used to store 3xFITS_NAXIS=3
  files.

  @note This function changes some of the components of aParams. Specifically,
        dx, dy, and dlambda may be changed after calling this function!

  @qa Using the INM, different astronomical scenes can be created to be
      used for quality checks.
      When creating a scene with a few well separated point sources, one knows
      the flux of each source at each wavelength. If this function works
      correctly, the flux should be conserved and can be compared to the
      expected value (using some tolerance).
      If using a scene with sky emission lines, one can measure the wavelengths
      and dispersions in a few of the sky lines. They should be at the same
      wavelength for all spaxels and this position should agree with the
      position determined from the INM-created data.
      Additionally, a (visual) comparison of monochromatic maps created from
      INM input frames can be compared to maps derived from output cubes created
      by this routine. If all intermediate calibrations were derived correctly,
      both should look similar.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         the input pixel table or input param structure are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         the input pixel table does not contain full geometry information}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, return NULL,
         the WCS in the pixel table is neither in pixels nor degrees}
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return NULL,
         computed output size in at least one coordinate is not positive}
  @error{just create empty datacube and possibly return pixel grid,
         given method is MUSE_RESAMPLE_NONE}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, return NULL, given method is unknown}
  @error{return NULL\, propagate error code, resampling fails}
 */
/*---------------------------------------------------------------------------*/
muse_datacube *
muse_resampling_cube(muse_pixtable *aPixtable, muse_resampling_params *aParams,
                     muse_pixgrid **aGrid)
{
  cpl_ensure(aPixtable && aParams, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(muse_pixtable_get_type(aPixtable) == MUSE_PIXTABLE_TYPE_FULL,
             CPL_ERROR_ILLEGAL_INPUT, NULL);
  muse_pixtable_wcs wcstype = muse_pixtable_wcs_check(aPixtable);
  cpl_ensure(wcstype == MUSE_PIXTABLE_WCS_CELSPH ||
             wcstype == MUSE_PIXTABLE_WCS_PIXEL, CPL_ERROR_UNSUPPORTED_MODE,
             NULL);

  /* compute or set the size of the output grid depending on *
   * the inputs and the data available in the pixel table    */
  muse_resampling_check_deltas(aPixtable, aParams);
  /* compute output sizes; wavelength is different in that it is *
   * more useful to contain partly empty areas within the field  *
   * for the extreme ends, so use ceil()                         */
  int xsize = 0, ysize = 0, zsize = 0;
  muse_resampling_compute_size(aPixtable, aParams, &xsize, &ysize, &zsize);
  muse_resampling_override_size(&xsize, &ysize, &zsize, aParams);
  cpl_ensure(xsize > 0 && ysize > 0 && zsize > 0, CPL_ERROR_ILLEGAL_OUTPUT,
             NULL);

  double time = cpl_test_get_walltime();

  /* create the structure for the output datacube */
  muse_datacube *cube = cpl_calloc(1, sizeof(muse_datacube));

  /* copy and clean up incoming FITS headers */
  cube->header = cpl_propertylist_duplicate(aPixtable->header);
  cpl_propertylist_erase_regexp(cube->header,
                                "^SIMPLE$|^BITPIX$|^NAXIS|^EXTEND$|^XTENSION$|"
                                "^DATASUM$|^DATAMIN$|^DATAMAX$|^DATAMD5$|"
                                "^PCOUNT$|^GCOUNT$|^HDUVERS$|^BLANK$|"
                                "^BZERO$|^BSCALE$|^CHECKSUM$|^INHERIT$|"
                                "^EXTNAME$|"MUSE_WCS_KEYS"|"MUSE_HDR_PT_REGEXP,
                                0);
  /* set data unit depending on data unit type in the pixel table */
  cpl_propertylist_update_string(cube->header, "BUNIT",
                                 cpl_table_get_column_unit(aPixtable->table,
                                                           MUSE_PIXTABLE_DATA));
  /* set NAXIS for later handling of the WCS */
  cpl_propertylist_update_int(cube->header, "NAXIS", 3);
  cpl_propertylist_update_int(cube->header, "NAXIS1", xsize);
  cpl_propertylist_update_int(cube->header, "NAXIS2", ysize);
  cpl_propertylist_update_int(cube->header, "NAXIS3", zsize);
  /* if pixel table was astrometrically calibrated, use its WCS headers *
   * Axis 1: x or RA, axis 2: y or DEC, axis 3: lambda                  */
  if (wcstype == MUSE_PIXTABLE_WCS_CELSPH) {
    cpl_propertylist_copy_property_regexp(cube->header, aPixtable->header,
                                          MUSE_WCS_KEYS, 0);
    cpl_propertylist_update_double(cube->header, "CD1_1", -aParams->dx);
    cpl_propertylist_update_double(cube->header, "CD2_2", aParams->dy);
    cpl_propertylist_update_double(cube->header, "CD1_2", 0.);
    cpl_propertylist_update_double(cube->header, "CD2_1", 0.);
    /* correct CRPIX by the central pixel coordinate */
    cpl_propertylist_update_double(cube->header, "CRPIX1",
                                   muse_pfits_get_crpix(cube->header, 1)
                                   + (1. + xsize) / 2.);
    cpl_propertylist_update_double(cube->header, "CRPIX2",
                                   muse_pfits_get_crpix(cube->header, 2)
                                   + (1. + ysize) / 2.);
    /* the pixel table had WCSAXES=2 which should not be propagated to the cube! */
    cpl_propertylist_erase(cube->header, "WCSAXES");
  } else {
    /* add basic WCS headers that are also required by the functions below. */
    cpl_propertylist_append_double(cube->header, "CRPIX1", 1.);
    cpl_propertylist_append_double(cube->header, "CRVAL1",
                                   cpl_propertylist_get_float(aPixtable->header,
                                                              MUSE_HDR_PT_XLO));
    /* for linear axes CTYPE doesn't matter, as long as it's not in 4-3 form, *
     * see Greisen & Calabretta 2002 A&A 395, 1061                            */
    cpl_propertylist_append_string(cube->header, "CTYPE1", "PIXEL");
    cpl_propertylist_append_string(cube->header, "CUNIT1", "pixel");
    cpl_propertylist_append_double(cube->header, "CD1_1", aParams->dx);
    cpl_propertylist_append_double(cube->header, "CRPIX2", 1.);
    cpl_propertylist_append_double(cube->header, "CRVAL2",
                                   cpl_propertylist_get_float(aPixtable->header,
                                                              MUSE_HDR_PT_YLO));
    cpl_propertylist_append_string(cube->header, "CTYPE2", "PIXEL");
    cpl_propertylist_append_string(cube->header, "CUNIT2", "pixel");
    cpl_propertylist_append_double(cube->header, "CD2_2", aParams->dy);
    /* fill in empty cross-terms of the CDi_j matrix */
    cpl_propertylist_append_double(cube->header, "CD1_2", 0.);
    cpl_propertylist_append_double(cube->header, "CD2_1", 0.);
  }
  switch (aParams->tlambda) {
  case MUSE_RESAMPLING_DISP_AWAV:
    cpl_propertylist_append_string(cube->header, "CTYPE3", "AWAV");
    break;
  case MUSE_RESAMPLING_DISP_AWAV_LOG:
    cpl_propertylist_append_string(cube->header, "CTYPE3", "AWAV-LOG");
    break;
  case MUSE_RESAMPLING_DISP_WAVE:
    cpl_propertylist_append_string(cube->header, "CTYPE3", "WAVE");
    break;
  case MUSE_RESAMPLING_DISP_WAVE_LOG:
    cpl_propertylist_append_string(cube->header, "CTYPE3", "WAVE-LOG");
    break;
  default:
    cpl_propertylist_append_string(cube->header, "CTYPE3", "UNKNOWN");
  } /* switch */
  cpl_propertylist_append_string(cube->header, "CUNIT3", "Angstrom");
  cpl_propertylist_append_double(cube->header, "CD3_3", aParams->dlambda);
  cpl_propertylist_append_double(cube->header, "CRPIX3", 1.);
  cpl_propertylist_append_double(cube->header, "CRVAL3",
                                 cpl_propertylist_get_float(aPixtable->header,
                                                            MUSE_HDR_PT_LLO));
  /* fill in empty cross-terms of the CDi_j matrix */
  cpl_propertylist_append_double(cube->header, "CD1_3", 0.);
  cpl_propertylist_append_double(cube->header, "CD2_3", 0.);
  cpl_propertylist_append_double(cube->header, "CD3_1", 0.);
  cpl_propertylist_append_double(cube->header, "CD3_2", 0.);
  /* transfer flat-spectrum correction status */
  if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_FFCORR)) {
    cpl_propertylist_append_bool(cube->header, MUSE_HDR_FLUX_FFCORR, CPL_TRUE);
    cpl_propertylist_set_comment(cube->header, MUSE_HDR_FLUX_FFCORR,
                                 MUSE_HDR_FLUX_FFCORR_C);
  }
  /* after everything has been transferred from the pixel table headers, *
   * see if we want to override anything from an OUTPUT_WCS setup        */
  muse_resampling_override_wcs(cube, aParams);
  /* check, if we need to move the zeropoint to fit the data into the grid */
  muse_resampling_fit_data_to_grid(cube, aPixtable);

  if (aParams->method < MUSE_RESAMPLE_NONE) {
    /* fill the cube for the data */
    cube->data = cpl_imagelist_new();
    cube->dq = cpl_imagelist_new();
    cube->stat = cpl_imagelist_new();
    int i;
    for (i = 0; i < zsize; i++) {
      cpl_image *image = cpl_image_new(xsize, ysize, CPL_TYPE_FLOAT);
      /* set as last image in the list to extend the list size by one */
      cpl_imagelist_set(cube->data, image, i);
      /* can use the same (empty) image for the variance for now */
      cpl_imagelist_set(cube->stat, cpl_image_duplicate(image), i);

      /* the other two are part of their lists now, we can reuse *
       * the variable without leaking                            */
      image = cpl_image_new(xsize, ysize, CPL_TYPE_INT);
      /* pre-fill with Euro3D status for outside data range */
      cpl_image_add_scalar(image, EURO3D_OUTSDRANGE);
      cpl_imagelist_set(cube->dq, image, i);
    } /* for i (all wavelength planes) */
  } /* if method not MUSE_RESAMPLE_NONE */

  muse_utils_memory_dump("muse_resampling_cube() before pixgrid");
  /* convert the pixel table into a pixel grid */
  muse_pixgrid *grid = muse_pixgrid_create(aPixtable, cube->header,
                                           xsize, ysize, zsize);

  if (aParams->crsigma > 0) {
    muse_resampling_crreject(aPixtable, grid, aParams->crtype,
                             aParams->crsigma);
  }
  muse_utils_memory_dump("muse_resampling_cube() after pixgrid and crreject");

  double timeinit = cpl_test_get_walltime(),
         cpuinit = cpl_test_get_cputime();

  /* do the resampling */
  cpl_error_code rc = CPL_ERROR_NONE;
  switch (aParams->method) {
  case MUSE_RESAMPLE_NEAREST:
    cpl_msg_info(__func__, "Starting resampling, using method \"nearest\"");
    rc = muse_resampling_cube_nearest(cube, aPixtable, grid);
    break;
  case MUSE_RESAMPLE_WEIGHTED_RENKA:
    cpl_msg_info(__func__, "Starting resampling, using method \"renka\" "
                 "(critical radius rc=%f, loop distance ld=%d)", aParams->rc,
                 aParams->ld);
    rc = muse_resampling_cube_weighted(cube, aPixtable, grid, aParams);
    break;
  case MUSE_RESAMPLE_WEIGHTED_LINEAR:
  case MUSE_RESAMPLE_WEIGHTED_QUADRATIC:
  case MUSE_RESAMPLE_WEIGHTED_LANCZOS:
    cpl_msg_info(__func__, "Starting resampling, using method \"%s\" (loop "
                 "distance ld=%d)",
                 aParams->method == MUSE_RESAMPLE_WEIGHTED_LINEAR
                   ? "linear"
                   : (aParams->method == MUSE_RESAMPLE_WEIGHTED_QUADRATIC
                      ? "quadratic"
                      : "lanczos"),
                 aParams->ld);
    rc = muse_resampling_cube_weighted(cube, aPixtable, grid, aParams);
    break;
  case MUSE_RESAMPLE_WEIGHTED_DRIZZLE:
    cpl_msg_info(__func__, "Starting resampling, using method \"drizzle\" "
                 "(pixfrac f=%.3f,%.3f,%.3f, loop distance ld=%d)",
                 aParams->pfx, aParams->pfy, aParams->pfl, aParams->ld);
    rc = muse_resampling_cube_weighted(cube, aPixtable, grid, aParams);
    break;
  case MUSE_RESAMPLE_NONE:
    cpl_msg_debug(__func__, "Method %d (no resampling)", aParams->method);
    break;
  default:
    cpl_msg_error(__func__, "Don't know this resampling method: %d",
                  aParams->method);
    cpl_error_set(__func__, CPL_ERROR_UNSUPPORTED_MODE);
    rc = CPL_ERROR_UNSUPPORTED_MODE;
  }
  muse_utils_memory_dump("muse_resampling_cube() after resampling");

  double timefini = cpl_test_get_walltime(),
         cpufini = cpl_test_get_cputime();

  /* now that we have resampled we can either remove the pixel grid or save it */
  if (aGrid) {
    *aGrid = grid;
  } else {
    muse_pixgrid_delete(grid);
  }

  cpl_msg_debug(__func__, "resampling took %.3fs (wall-clock) and %.3fs "
                "(%.3fs CPU, %d CPUs) for muse_resampling_cube*() alone",
                timefini - time, timefini - timeinit, cpufini - cpuinit,
                omp_get_max_threads());
  if (rc != CPL_ERROR_NONE) {
    cpl_msg_error(__func__, "resampling failed: %s", cpl_error_get_message());
    muse_datacube_delete(cube);
    return NULL;
  }

  muse_utils_memory_dump("muse_resampling_cube() end");
  return cube;
} /* muse_resampling_cube() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Integrate a pixel table / pixel grid along the wavelength direction.
  @param   aPixtable   the input pixel table
  @param   aGrid       the input pixel grid
  @param   aCube       the datacube (only the header component is used)
  @param   aFilter     the filter response curve
  @param   aParams     the structure of resampling parameters
  @return  A muse_image * of the field of view or NULL on error.

  Loop through all pixels of the output image and integrate along the wavelength
  direction, using the requested resampling scheme to interpolate pixels of all
  wavelengths onto the output 2D image. The filter response curve is used weight
  the pixel data values according to their wavelength.

  @note Only weighted resampling types are allowed, MUSE_RESAMPLE_NEAREST is
        not supported.

  Optionally, if the environment variable MUSE_COLLAPSE_USE_VARIANCE is set to
  something positive, the variance information in the input pixel table will be
  used to weight the data according to their S/N.

  XXX this function copies large parts of muse_resampling_cube_weighted().

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aPixtable\, its table component\, aGrid\, aCube\, its header component\, or aParams are NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         the resampling method passed in aParams is MUSE_RESAMPLE_NEAREST or unknown}
  @error{integrate over the whole wavelength range, aFilter is NULL}
 */
/*---------------------------------------------------------------------------*/
muse_image *
muse_resampling_collapse_pixgrid(muse_pixtable *aPixtable, muse_pixgrid *aGrid,
                                 muse_datacube *aCube, const muse_table *aFilter,
                                 muse_resampling_params *aParams)
{
  cpl_ensure(aPixtable && aPixtable->table && aGrid && aParams &&
             aCube && aCube->header, CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(aParams->method < MUSE_RESAMPLE_NONE &&
             aParams->method > MUSE_RESAMPLE_NEAREST, CPL_ERROR_ILLEGAL_INPUT,
             NULL);

  muse_wcs *wcs = muse_wcs_new(aCube->header);
  wcs->iscelsph = muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_CELSPH;
  cpl_errorstate prestate = cpl_errorstate_get();
  float *xpos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_YPOS),
        *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA),
        *stat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT),
        *wght = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_WEIGHT);
  if (!cpl_errorstate_is_equal(prestate)) {
    cpl_errorstate_set(prestate); /* recover from missing weight column */
  }
  int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);

  /* If our data was astrometrically calibrated, we need to scale the *
   * data units to the pixel size in all three dimensions so that the *
   * radius computation works again.                                  *
   * Otherwise dx~5.6e-5deg won't contribute to the weighting at all. */
  double xnorm = 1., ynorm = 1.,
         ptxoff = 0., /* zero by default ...   */
         ptyoff = 0.; /* for pixel coordinates */
  if (wcs->iscelsph) {
    muse_wcs_get_scales(aPixtable->header, &xnorm, &ynorm);
    xnorm = 1. / xnorm;
    ynorm = 1. / ynorm;
    /* need to use the real coordinate offset for celestial spherical */
    ptxoff = muse_pfits_get_crval(aPixtable->header, 1);
    ptyoff = muse_pfits_get_crval(aPixtable->header, 2);
  }
  /* scale the input critical radius by the voxel radius */
  double renka_rc = aParams->rc                    /* XXX beware of rotation! */
    * sqrt(pow(wcs->cd11*xnorm, 2) + pow(wcs->cd22*xnorm, 2));
  /* loop distance (to take into account surrounding pixels) verification */
  int ld = aParams->ld;
  if (ld <= 0) {
    ld = 1;
    cpl_msg_info(__func__, "Overriding loop distance ld=%d", ld);
  }
  /* pixel sizes in the spatial directions, scaled by pixfrac, and *
   * output pixel sizes (absolute values), as needed for drizzle   */
  double xsz = aParams->pfx / xnorm,
         ysz = aParams->pfy / ynorm,
         xout = fabs(wcs->cd11), yout = fabs(wcs->cd22);

  muse_image *image = muse_image_new();
  image->data = cpl_image_new(aGrid->nx, aGrid->ny, CPL_TYPE_FLOAT);
  image->dq = cpl_image_new(aGrid->nx, aGrid->ny, CPL_TYPE_INT);
  image->stat = cpl_image_new(aGrid->nx, aGrid->ny, CPL_TYPE_FLOAT);
  image->header = cpl_propertylist_duplicate(aCube->header);
  cpl_propertylist_erase_regexp(image->header, "^C...*3$|^CD3_.$", 0);
  float *pdata = cpl_image_get_data_float(image->data),
        *pstat = cpl_image_get_data_float(image->stat);
  int *pdq = cpl_image_get_data_int(image->dq);

  /* check if we need to use the variance for weighting */
  cpl_boolean usevariance = CPL_FALSE;
  if (getenv("MUSE_COLLAPSE_USE_VARIANCE") &&
      atoi(getenv("MUSE_COLLAPSE_USE_VARIANCE")) > 0) {
    usevariance = CPL_TRUE;
  }

  /* find filter range and coverage fraction */
  cpl_table *filter = aFilter && aFilter->table ? aFilter->table : NULL;
  double lmin, lmax;
  if (filter) {
    const double *flbda = cpl_table_get_data_double_const(filter, "lambda"),
                 *fresp = cpl_table_get_data_double_const(filter, "throughput");
    int l = 0, nl = cpl_table_get_nrow(filter);
    while (l < nl && fabs(fresp[l]) < DBL_EPSILON) {
      lmin = flbda[l++];
    }
    l = nl - 1;
    while (l > 0 && fabs(fresp[l]) < DBL_EPSILON) {
      lmax = flbda[l--];
    }
    cpl_msg_debug(__func__, "filter wavelength range %.1f..%.1fA", lmin, lmax);
    /* compute filter coverage and propagate filter properties */
    double ffraction = muse_utils_filter_fraction(aFilter, lmin, lmax);
    muse_utils_filter_copy_properties(image->header, aFilter, ffraction);
  } else {
    lmin = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LLO);
    lmax = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LHI);
    cpl_msg_debug(__func__, "full wavelength range %.1f..%.1fA", lmin, lmax);
  }

  int i;
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aParams, aGrid, data, dq, filter, lbda, ld, lmax, lmin, pdata,\
                 pdq, pstat, ptxoff, ptyoff, renka_rc, stat, usevariance, wcs, \
                 wght, xnorm, xout, xpos, xsz, ynorm, yout, ypos, ysz)
  for (i = 0; i < aGrid->nx; i++) {
    int j;
    for (j = 0; j < aGrid->ny; j++) {
      /* x and y position of center of current grid cell (i, j start at 0) */
      double x, y;
      if (wcs->iscelsph) {
        muse_wcs_celestial_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
      } else {
        muse_wcs_projplane_from_pixel_fast(wcs, i + 1, j + 1, &x, &y);
      }
      double sumdata = 0, sumstat = 0, sumweight = 0;
      cpl_size npoints = 0;

      /* loop through surrounding cells and their contained pixels */
      int i2;
      for (i2 = i - ld; i2 <= i + ld; i2++) {
        int j2;
        for (j2 = j - ld; j2 <= j + ld; j2++) {
          /* loop over full wavelength range! */
          int l2;
          for (l2 = 0; l2 < aGrid->nz; l2++) {
            cpl_size idx2 = muse_pixgrid_get_index(aGrid, i2, j2, l2, CPL_FALSE),
                     n, n_rows2 = muse_pixgrid_get_count(aGrid, idx2);
            const cpl_size *rows2 = muse_pixgrid_get_rows(aGrid, idx2);
            for (n = 0; n < n_rows2; n++) {
              if (dq[rows2[n]]) { /* exclude all bad pixels */
                continue;
              }
              if (usevariance && !isnormal(stat[rows2[n]])) {
                continue;  /* weighting by inv. variance doesn`t work with extremes */
              }
              if (lbda[rows2[n]] > lmax || lbda[rows2[n]] < lmin) {
                continue; /* exclude pixels outside the filter range */
              }

              double dx = fabs(x - (xpos[rows2[n]] + ptxoff)),
                     dy = fabs(y - (ypos[rows2[n]] + ptyoff)),
                     r2 = 0;
              if (wcs->iscelsph) {
                /* Make the RA distance comparable to pixel    *
                 * sizes, see muse_resampling_cube_weighted(). */
                dx *= cos(y * CPL_MATH_RAD_DEG);
              }
              if (aParams->method != MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
                dx *= xnorm;
                dy *= ynorm;
                r2 = dx*dx + dy*dy;
              }
              double weight = 0.;
              if (aParams->method == MUSE_RESAMPLE_WEIGHTED_RENKA) {
                weight = muse_resampling_weight_function_renka(sqrt(r2), renka_rc);
              } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
                weight = muse_resampling_weight_function_drizzle(xsz, ysz, 1.,
                                                                 xout, yout, 1.,
                                                                 dx, dy, 0.);
              } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_LINEAR) {
                weight = muse_resampling_weight_function_linear(sqrt(r2));
              } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_QUADRATIC) {
                weight = muse_resampling_weight_function_quadratic(r2);
              } else if (aParams->method == MUSE_RESAMPLE_WEIGHTED_LANCZOS) {
                weight = muse_resampling_weight_function_lanczos(dx, dy, 0., ld);
              }

              if (wght) { /* the pixel table does contain weights */
                /* apply it on top of the weight computed here */
                weight *= wght[rows2[n]];
              }
              /* apply the filter curve on top of the weights, if a filter *
               * was given; otherwise the weight stays unchanged           */
              if (filter) {
                weight *= muse_flux_response_interpolate(filter, lbda[rows2[n]],
                                                         NULL, MUSE_FLUX_RESP_FILTER);
              }
              if (usevariance) { /* weight by inverse variance */
                weight /= stat[rows2[n]];
              }
              sumweight += weight;
              sumdata += data[rows2[n]] * weight;
              sumstat += stat[rows2[n]] * weight*weight;
              npoints++;
            } /* for n (all pixels in grid cell) */
          } /* for l2 (lambda direction) */
        } /* for j2 (y direction) */
      } /* for i2 (x direction) */

      /* if no points were found, we cannot divide by the summed weight *
       * and don't need to set the output pixel value (it's 0 already), *
       * only set the relevant Euro3D bad pixel flag                    */
      if (!npoints || !isnormal(sumweight)) {
        pdq[i + j * aGrid->nx] = EURO3D_MISSDATA;
        continue;
      }

      /* divide results by weight of summed pixels */
      sumdata /= sumweight;
      sumstat /= sumweight*sumweight;
      pdata[i + j * aGrid->nx] = sumdata;
      pstat[i + j * aGrid->nx] = sumstat;
      pdq[i + j * aGrid->nx] = EURO3D_GOODPIXEL; /* now we can mark it as good */
    } /* for j (y direction) */
  } /* for i (x direction) */
  cpl_free(wcs);

  return image;
} /* muse_resampling_collapse_pixgrid() */


/*----------------------------------------------------------------------------*
 *                             Resampling and collapsing in 2D                *
 *----------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Do the resampling from pixel grid into 2D image using nearest
           neighbor.
  @param   aImage      the MUSE image to fill
  @param   aPixtable   the input pixel table
  @param   aGrid       the input pixel grid (a 2D array)
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  Simply loop through all grid points wavelength by wavelength and set the data
  values of the one or closest data point.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aImage\, aPixtable\, or aGrid are NULL}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_image_nearest(muse_image *aImage, muse_pixtable *aPixtable,
                              muse_pixgrid *aGrid)
{
  cpl_ensure_code(aImage && aPixtable && aGrid, CPL_ERROR_NULL_INPUT);
  aImage->data = cpl_image_new(aGrid->nx, aGrid->nz,
                               CPL_TYPE_FLOAT);

  double crval2 = muse_pfits_get_crval(aImage->header, 2),
         cd22 = muse_pfits_get_cd(aImage->header, 2, 2);
  /* get all (relevant) table columns for easy pointer access */
  float *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA);
  float *imagedata = cpl_image_get_data_float(aImage->data);

#ifdef ESO_ENABLE_DEBUG
  int debug = 0;
  if (getenv("MUSE_DEBUG_NEAREST")) {
    debug = atoi(getenv("MUSE_DEBUG_NEAREST"));
  }
#endif

  int i;
  for (i = 0; i < aGrid->nx; i++) {
    int j;
    for (j = 0; j < aGrid->nz; j++) {
      cpl_size idx = muse_pixgrid_get_index(aGrid, i, 0, j, CPL_FALSE),
               n_rows = muse_pixgrid_get_count(aGrid, idx);
      const cpl_size *rows = muse_pixgrid_get_rows(aGrid, idx);
      if (n_rows == 1) {
        /* if there is only one pixel in the cell, just use it */
        imagedata[i + j * aGrid->nx] = data[rows[0]];
#ifdef ESO_ENABLE_DEBUG
        if (debug) {
          printf("only:    %f\n", data[rows[0]]);
          fflush(stdout);
        }
#endif
      } else if (n_rows >= 2) {
        /* loop through all available values and take the closest one */
        cpl_size n, nbest = -1;
        double dbest = FLT_MAX; /* some unlikely large value to start with*/
        for (n = 0; n < n_rows; n++) {
          /* the differences for the cell center and the current pixel; *
           * for simplicitly, just compare the difference in lambda     */
          double dlambda = fabs(j * cd22 + crval2 - lbda[rows[n]]);
#ifdef ESO_ENABLE_DEBUG
          if (debug) {
            printf("%f, %f, d: %f best: %f (%f)\n",
                   j * cd22 + crval2, lbda[rows[n]],
                   dlambda, dbest, data[rows[n]]);
          }
#endif
          if (dlambda < dbest) {
            nbest = n;
            dbest = dlambda;
          }
        }
        imagedata[i + j * aGrid->nx] = data[rows[nbest]];
#ifdef ESO_ENABLE_DEBUG
        if (debug) {
          printf("closest: %f\n", data[rows[nbest]]);
          fflush(stdout);
        }
#endif
      } else {
        /* npix == 0: do nothing, pixel stays zero */
      }
    } /* for j (y direction = wavelength planes) */
  } /* for i (x direction) */

  return CPL_ERROR_NONE;
} /* muse_resampling_image_nearest() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Do the resampling from pixel grid into 2D image using a weighting
           scheme.
  @param   aImage      the MUSE image to fill
  @param   aPixtable   the input pixel table
  @param   aGrid       the input pixel grid
  @return  CPL_ERROR_NONE on success, another cpl_error_code on failure

  Loop through all grid points wavelength by wavelength and compute the final
  data values using a suitable weighting scheme on the neighboring grid points
  in wavelength direction.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aImage\, aPixtable\, or aGrid are NULL}
 */
/*---------------------------------------------------------------------------*/
static cpl_error_code
muse_resampling_image_weighted(muse_image *aImage, muse_pixtable *aPixtable,
                               muse_pixgrid *aGrid)
{
  cpl_ensure_code(aImage && aPixtable && aGrid, CPL_ERROR_NULL_INPUT);
  aImage->data = cpl_image_new(aGrid->nx, aGrid->nz,
                               CPL_TYPE_FLOAT);

  double crval2 = muse_pfits_get_crval(aImage->header, 2),
         cd22 = muse_pfits_get_cd(aImage->header, 2, 2);
  /* get all (relevant) table columns for easy pointer access */
  float *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA);
  float *imagedata = cpl_image_get_data_float(aImage->data);

#ifdef ESO_ENABLE_DEBUG
  int debug = 0, debugx = 0, debugz = 0;
  if (getenv("MUSE_DEBUG_WEIGHTED")) {
    debug = atoi(getenv("MUSE_DEBUG_WEIGHTED"));
  }
  if (debug & 128) { /* need coordinates */
    if (getenv("MUSE_DEBUG_WEIGHTED_X")) {
      debugx = atoi(getenv("MUSE_DEBUG_WEIGHTED_X"));
      if (debugx < 1 || debugx > aGrid->nx) {
        debugx = 0;
      }
    }
    if (getenv("MUSE_DEBUG_WEIGHTED_Z")) {
      debugz = atoi(getenv("MUSE_DEBUG_WEIGHTED_Z"));
      if (debugz < 1 || debugz > aGrid->nz) {
        debugz = 0;
      }
    }
  }
#endif

  int ld = 1; /* loop distance, to take into account surrounding pixels */
  /* Renka critical radius; only 1.25 to 1.5 makes sense in *
   * this 1D interpolation, hardcode to 1.25 for the moment */
  double renka_rc = 1.25;
  /* scale critical radius by the scale factor in wavelength */
  renka_rc *= cd22;

  int i;
  for (i = 0; i < aGrid->nx; i++) {
    int j;
    for (j = 0; j < aGrid->nz; j++) {
      double sumdata = 0, sumweight = 0,
             lambda = j * cd22 + crval2;
      int npoints = 0;
#ifdef ESO_ENABLE_DEBUG
#define MAX_NPIX_2D 50 /* allow for quite a few pixels */
      int *pointlist = NULL;
      double *pointweights = NULL;
      if (debug & 128 && i+1 == debugx && j+1 == debugz) {
        pointlist = cpl_calloc(MAX_NPIX_2D, sizeof(int));
        pointweights = cpl_malloc(MAX_NPIX_2D * sizeof(double));
      }
#endif

      /* go through this and the surrounding cells and their contained pixels */
      int j2;
      for (j2 = j - ld; j2 <= j + ld; j2++) {
        cpl_size idx = muse_pixgrid_get_index(aGrid, i, 0, j2, CPL_FALSE),
                 n, n_rows = muse_pixgrid_get_count(aGrid, idx);
        const cpl_size *rows = muse_pixgrid_get_rows(aGrid, idx);
        for (n = 0; n < n_rows; n++) {
          double dlambda = fabs(lambda - lbda[rows[n]]),
                 weight = muse_resampling_weight_function_renka(dlambda,
                                                                renka_rc);
          sumweight += weight;
          sumdata += data[rows[n]] * weight;
          npoints++;
#ifdef ESO_ENABLE_DEBUG
          if (debug & 128 && i+1 == debugx && j+1 == debugz &&
              npoints-1 < MAX_NPIX_2D) {
            /* store row number instead of index, because we cannot be zero: */
            pointlist[npoints-1] = rows[n] + 1;
            pointweights[npoints-1] = weight;
          }

          if (debug & 256) {
            printf("  pixel %4d,%4d (%8"CPL_SIZE_FORMAT"): %2"CPL_SIZE_FORMAT
                   " %2"CPL_SIZE_FORMAT"   %f, %f -> %f ==> %f %f (%d)\n",
                   i, j2, idx, n, muse_pixgrid_get_count(aGrid, idx), dlambda,
                   data[muse_pixgrid_get_rows(aGrid, idx)[n]],
                   weight, sumweight, sumdata, npoints);
          }
#endif
        } /* for n (all pixels in grid cell) */
      } /* for j2 (x direction) */

#ifdef ESO_ENABLE_DEBUG
      if (debug & 128 && i+1 == debugx && j+1 == debugz) {
        printf("pixelnumber  weight   ");
        muse_pixtable_dump(aPixtable, 0, 0, 2);
        int m = -1;
        while (++m < MAX_NPIX_2D && pointlist[m] != 0) {
          /* access row using index again: */
          printf("%12d %8.5f    ", pointlist[m] - 1, pointweights[m]);
          muse_pixtable_dump(aPixtable, pointlist[m] - 1, 1, 0);
        }
        fflush(stdout);
        cpl_free(pointlist);
        cpl_free(pointweights);
      }
      if (debug) {
        fflush(stdout); /* flush the output in any debug case */
      }
#endif
      /* if no points were found, we cannot divide by the summed weight *
       * and don't need to set the output pixel value (it's 0 already)  */
      if (!npoints) {
        continue;
      }
      /* divide results by weight of summed pixels */
      imagedata[i + j * aGrid->nx] = sumdata / sumweight;
    } /* for j (y direction = wavelength planes) */
  } /* for i (x direction) */

  return CPL_ERROR_NONE;
} /* muse_resampling_image_weighted() */

/*---------------------------------------------------------------------------*/
/**
  @private
  @brief   Resample selected rows of a pixel table onto a 2 dimensional regular
           grid.
  @param   aPixtable   MUSE pixel table to resample
  @param   aMethod     Value corresponding to a resampling method
  @param   aDX         Step size in pixel direction
  @param   aDLambda    Step size in wavelength direction [Angstrom]
  @param   aLambdaMin  Lower wavelength limit
  @param   aLambdaMax  Upper wavelength limit
  @return  a muse_image * on success or NULL on error

  See muse_resampling_image() for remarks relevant for this function.

  aMethod is not checked for validity!
*/
/*---------------------------------------------------------------------------*/
static muse_image *
muse_resampling_image_selected(muse_pixtable *aPixtable,
                               muse_resampling_type aMethod,
                               double aDX, double aLambdaMin,
                               double aLambdaMax, double aDLambda)
{
  double dlambda = aDLambda;
  float xmin = 0.;
  muse_pixgrid *grid = muse_pixgrid_2d_create(aPixtable->table, aDX,
                                              aLambdaMin, aLambdaMax,
                                              dlambda, &xmin);

  /* create output image and add FITS header to it (we already need *
   * CRVAL2 and CD2_2 in the sub-functions called from here)        */
  muse_image *image = muse_image_new();
  image->header = cpl_propertylist_new();
  const char *unit = cpl_table_get_column_unit(aPixtable->table, "data");
  cpl_propertylist_append_string(image->header, "BUNIT", unit);
  /* copy input header and append the necessary WCS keywords         *
   * WCSAXES must preceed all other keywords, but defaults to NAXIS, *
   * so we can ignore this here for simplicity                       */
  cpl_propertylist_copy_property_regexp(image->header, aPixtable->header,
                                        MUSE_HDR_PT_REGEXP"|"MUSE_WCS_KEYS, 1);
  cpl_propertylist_append_double(image->header, "CRPIX1", 1.);
  cpl_propertylist_append_double(image->header, "CRPIX2", 1.);
  cpl_propertylist_append_double(image->header, "CRVAL1", 1.);
  cpl_propertylist_append_double(image->header, "CRVAL2", aLambdaMin);
  cpl_propertylist_append_double(image->header, "CD1_1", 1.);
  cpl_propertylist_append_double(image->header, "CD2_2", dlambda);
  /* units verified using FITS standard v3.0 */
  cpl_propertylist_append_string(image->header, "CUNIT1", "pixel");
  cpl_propertylist_append_string(image->header, "CUNIT2", "Angstrom");
  /* for linear axes CTYPE doesn't matter, as long as it's not in 4-3 form, *
   * see Greisen & Calabretta 2002 A&A 395, 1061                            */
  cpl_propertylist_append_string(image->header, "CTYPE1", "PIXEL");
  /* although DS9 seems to like "LAMBDA" better, the type (for the wavelength *
   * direction) was verified using Greisen et al. 2006 A&A 446, 747           */
  cpl_propertylist_append_string(image->header, "CTYPE2", "AWAV");
  /* fill in empty cross-terms of the CDi_j matrix */
  cpl_propertylist_append_double(image->header, "CD1_2", 0.);
  cpl_propertylist_append_double(image->header, "CD2_1", 0.);

  /* do the resampling */
  cpl_error_code rc = CPL_ERROR_NONE;
  switch (aMethod) {
  case MUSE_RESAMPLE_NEAREST:
    rc = muse_resampling_image_nearest(image, aPixtable, grid);
    break;
  default: /* MUSE_RESAMPLE_WEIGHTED_RENKA */
    /* the caller is responsible for checking for other methods */
    rc = muse_resampling_image_weighted(image, aPixtable, grid);
  }

  muse_pixgrid_delete(grid);
  if (rc != CPL_ERROR_NONE) {
    cpl_msg_error(__func__, "resampling failed: %s", cpl_error_get_message());
    muse_image_delete(image);
    return NULL;
  }

  return image;
} /* muse_resampling_image_selected() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Resample a pixel table onto a two-dimensional regular grid.
  @param   aPixtable   the MUSE pixel table to resample
  @param   aMethod     the value corresponding to a resampling method
  @param   aDX         Step size in pixel direction
  @param   aDLambda    the step size in wavelength direction (in Angstrom)
  @return  a muse_image * on success or NULL on error
  @remark  This function can be used instead of muse_resampling_cube() or
           muse_resampling_euro3d() for visual assessment of the wavelength
           calibration quality in the muse_wavecal recipe or for sky subtraction
           during post-processing.
  @remark  The output is an image in which the y-axis is the wavelength
           direction and the x-axis represents the spatial domain within the
           slices, with all slices placed adjacent.
  @remark  aMethod can be either MUSE_RESAMPLE_WEIGHTED_RENKA or MUSE_RESAMPLE_NEAREST
           (see Sect. 2.2.2 and 2.2.4 of the DRLDesign document). Other
           interpolation methods are not supported.
  @remark  No horizontal interpolation (in cross-dispersion direction) is done.
           These resampling methods only use information in the wavelength
           direction to find the output value, as it is not clear what the real
           spatial relationship of neighboring grid points is when stored in a
           2D grid.

  Loop through all the pixels in the regular output grid and interpolate as
  discussed in DRLDesign document (2.2). The result is stored in a muse_image
  structure, so that the headers and the data component of this image can be
  saved as a FITS NAXIS=2 image.

  @qa This method is only used for quick visual check, good quality is not
      expected.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL, the input pixel table is NULL}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, return NULL,
         the WCS in the pixel table is neither in pixels nor degrees}
  @error{set CPL_ERROR_UNSUPPORTED_MODE\, return NULL, given method is unknown}
  @error{set CPL_ERROR_ILLEGAL_OUTPUT\, return NULL,
         computed output size in at least one coordinate is not positive}
  @error{return NULL\, propagate error code, resampling fails}
 */
/*---------------------------------------------------------------------------*/
muse_image *
muse_resampling_image(muse_pixtable *aPixtable, muse_resampling_type aMethod,
                      double aDX, double aDLambda)
{
  cpl_ensure(aPixtable, CPL_ERROR_NULL_INPUT, NULL);
  if (aDLambda == 0.0) {
    aDLambda = kMuseSpectralSamplingA;
  }
  muse_pixtable_wcs wcstype = muse_pixtable_wcs_check(aPixtable);
  cpl_ensure(wcstype == MUSE_PIXTABLE_WCS_CELSPH ||
             wcstype == MUSE_PIXTABLE_WCS_PIXEL, CPL_ERROR_UNSUPPORTED_MODE,
             NULL);

  /* check method and give info message */
  switch (aMethod) {
  case MUSE_RESAMPLE_NEAREST:
    cpl_msg_info(__func__, "Using nearest neighbor sampling (%d) along "
                 "wavelengths.", aMethod);
    break;
  case MUSE_RESAMPLE_WEIGHTED_RENKA:
    cpl_msg_info(__func__, "Using renka-weighted interpolation (%d) along "
                 "wavelengths.", aMethod);
    break;
  default:
    cpl_msg_error(__func__, "Don't know this resampling method: %d", aMethod);
    cpl_error_set(__func__, CPL_ERROR_UNSUPPORTED_MODE);
    return NULL;
  }

  /* compute the size of the output grid depending on the inputs and the *
   * data available in the pixel table                                   */
  float lmin = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LLO),
        lmax = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LHI);

  if (muse_pixtable_get_type(aPixtable) == MUSE_PIXTABLE_TYPE_SIMPLE) {
    /* Pretend that we are dealing with only one slice.           *
     * We can do that because the pixel table was created in such *
     * a way that no slice overlaps in x-direction with another.  */
    return muse_resampling_image_selected(aPixtable, aMethod,
                                          aDX == 0.0 ? 1. : aDX,
                                          lmin, lmax, aDLambda);
  }

  /* For pixel tables with full geometry information, resample each *
   * slice of each IFU into an image one by one and stack them next *
   * to each other (horizontally) in a large output image.          */
  muse_pixtable **slice_pixtable = muse_pixtable_extracted_get_slices(aPixtable);
  int n_slices = muse_pixtable_extracted_get_size(slice_pixtable);

  double dx = aDX;
  if (dx == 0.0) {
    if (muse_pixtable_wcs_check(aPixtable) == MUSE_PIXTABLE_WCS_PIXEL) {
      dx = 1.0;
    } else {
      double xsc, ysc;
      muse_wcs_get_scales(aPixtable->header, &xsc, &ysc);
      /* XXX need such an extra 1.2 factor to get good results: */
      dx = 1.20 * xsc;
    }
  }
  cpl_msg_debug(__func__, "Resampling %d slices to a 2D image, using bins of"
                " %e %s x %.3f Angstrom", n_slices, dx,
                cpl_table_get_column_unit(aPixtable->table, MUSE_PIXTABLE_XPOS),
                aDLambda);

  /* build one stacked image per slice */
  muse_image *slice_image[n_slices];
  int i_slice;
  #pragma omp parallel for default(none)                 /* as req. by Ralf */ \
          shared(aDLambda, aMethod, dx, lmax, lmin, n_slices,         \
                 slice_image, slice_pixtable)
  for (i_slice = 0; i_slice < n_slices; i_slice++) {
#if 0
    cpl_msg_debug(__func__, "slice pixel table %d", i_slice + 1);
#endif
    muse_pixtable *pt = slice_pixtable[i_slice];
    if (muse_pixtable_get_nrow(pt) < 1) {
      slice_image[i_slice] = NULL;
      continue;
    }
    slice_image[i_slice] = muse_resampling_image_selected(pt, aMethod, dx,
                                                          lmin, lmax, aDLambda);
  } /* for i_slice */

  /* concatenate all images into one large output image */
  muse_image *image = muse_image_new();
  for (i_slice = 0; i_slice < n_slices; i_slice++) {
    muse_image *slice = slice_image[i_slice];
#if 0
    cpl_msg_debug(__func__, "slice %d: %ldx%ld", i_slice + 1,
                  cpl_image_get_size_x(slice->data), cpl_image_get_size_y(slice->data));
#endif
    if (slice == NULL) {
      continue;
    }
    if (!image->header) {
      /* use FITS header from the first slice */
      image->header = cpl_propertylist_duplicate(slice->header);
    }
    cpl_image *data = muse_cplimage_concat_x(image->data, slice->data);
    cpl_image_delete(image->data);
    image->data = data;
    if (slice->dq) {
      cpl_image *dq = muse_cplimage_concat_x(image->dq, slice->dq);
      cpl_image_delete(image->dq);
      image->dq = dq;
    }
    if (slice->stat) {
      cpl_image *stat = muse_cplimage_concat_x(image->stat, slice->stat);
      cpl_image_delete(image->stat);
      image->stat = stat;
    }
    muse_image_delete(slice);
    slice_image[i_slice] = NULL;
  } /* for i_slice */
  muse_pixtable_extracted_delete(slice_pixtable);

  /* transfer flat-spectrum correction status */
  if (cpl_propertylist_has(aPixtable->header, MUSE_HDR_PT_FFCORR)) {
    cpl_propertylist_append_bool(image->header, MUSE_HDR_FLUX_FFCORR, CPL_TRUE);
    cpl_propertylist_set_comment(image->header, MUSE_HDR_FLUX_FFCORR,
                                 MUSE_HDR_FLUX_FFCORR_C);
  }

  return image;
} /* muse_resampling_image() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Resample the selected pixels of a pixel table into a spectrum.
  @param   aPixtable  The MUSE pixel table to resample
  @param   aBinwidth  The bin width to use.
  @return  A table with a spectrum or NULL on error.

  This function carries out a linear interpolation of all selected pixels of
  a pixel table into a 1D spectrum. The output spectrum is represented as a
  table and may not be contiguous.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aPixtable or one of its components is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aPixtable does not match the definition of a pixel table}
 */
/*---------------------------------------------------------------------------*/
cpl_table *
muse_resampling_spectrum(muse_pixtable *aPixtable, double aBinwidth)
{
  cpl_ensure(aPixtable && aPixtable->header && aPixtable->table,
             CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(muse_cpltable_check(aPixtable->table, muse_pixtable_def)
             == CPL_ERROR_NONE, CPL_ERROR_ILLEGAL_INPUT, NULL);

  /* determine spectral range */
  double lmin = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LLO);
  double lmax = cpl_propertylist_get_float(aPixtable->header, MUSE_HDR_PT_LHI);
  cpl_size lsize = floor((lmax - lmin) / aBinwidth) + 2;

  /* create the empty spectral table */
  cpl_table *spectrum = muse_cpltable_new(muse_dataspectrum_def, lsize);
  cpl_table_fill_column_window(spectrum, "lambda", 0, lsize, 0);
  cpl_table_fill_column_window(spectrum, "data", 0, lsize, 0);
  cpl_table_fill_column_window(spectrum, "stat", 0, lsize, 0);
  cpl_table_fill_column_window(spectrum, "dq", 0, lsize, 0);
  double *spec_data = cpl_table_get_data_double(spectrum, "data");
  double *spec_stat = cpl_table_get_data_double(spectrum, "stat");
  double *spec_lbda = cpl_table_get_data_double(spectrum, "lambda");
  /* inherit spectral data units from the pixel table */
  cpl_table_set_column_unit(spectrum, "data",
                            cpl_table_get_column_unit(aPixtable->table,
                                                      MUSE_PIXTABLE_DATA));
  cpl_table_set_column_unit(spectrum, "stat",
                            cpl_table_get_column_unit(aPixtable->table,
                                                      MUSE_PIXTABLE_STAT));

  /* add a temporary column for the integrated weight of each bin */
  cpl_table_new_column(spectrum, "weight", CPL_TYPE_DOUBLE);
  cpl_table_fill_column_window(spectrum, "weight", 0, lsize, 0);
  double *spec_weight = cpl_table_get_data_double(spectrum, "weight");

  /* accessors for the pixel table */
  float *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA);
  float *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA);
  float *stat = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_STAT);
  float *wght = NULL;
  if (cpl_table_has_column(aPixtable->table, MUSE_PIXTABLE_WEIGHT)) {
    wght = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_WEIGHT);
  }
  int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);

  /* loop through all selected pixel table rows and sort them *
   * into the output bins, assigning them linear weights      */
  cpl_array *asel = cpl_table_where_selected(aPixtable->table);
  const cpl_size *sel = cpl_array_get_data_cplsize_const(asel);
  cpl_size isel, nsel = cpl_array_get_size(asel);
  for (isel = 0; isel < nsel; isel++) {
    cpl_size i_row = sel[isel];
    if ((dq[i_row] != EURO3D_GOODPIXEL)) {
      continue;
    }
    // We ignore "inf" and "nan" values here.
    // See http://urania1.univ-lyon1.fr/mpdaf/ticket/374
    if (! isfinite(data[i_row])) {
      continue;
    }
    /* compute target bins and weights */
    double l = (lbda[i_row] - lmin) / aBinwidth;
    if (l < 0) l = 0;
    cpl_size il = floor(l);
    l -= il;
    double w0 = 1-l;
    double w1 = l;
    if (wght) {
      w0 *= wght[i_row];
      w1 *= wght[i_row];
    }
    /* weight the data in the two involved bins */
    spec_data[il] += w0 * data[i_row];
    spec_data[il+1] += w1 * data[i_row];
    spec_stat[il] += w0 * stat[i_row];
    spec_stat[il+1] += w1 * stat[i_row];
    spec_weight[il] += w0;
    spec_weight[il+1] += w1;
  } /* for isel (selected pixel table rows) */
  cpl_array_delete(asel);

  /* assign wavelengths and erase empty spectral bins */
  cpl_size ispec;
  for (ispec = 0; ispec < lsize; ispec++) {
    if (spec_weight[ispec] > 0) {
      spec_lbda[ispec] = ispec * aBinwidth + lmin;
      cpl_table_unselect_row(spectrum, ispec);
    }
  } /* for ispec (all spectrum points) */
  cpl_table_erase_selected(spectrum);

  /* apply the weights and delete the temporary weight column */
  cpl_table_divide_columns(spectrum, "data", "weight");
  cpl_table_divide_columns(spectrum, "stat", "weight");
  cpl_table_erase_column(spectrum, "weight");
  return spectrum;
} /* muse_resampling_spectrum() */

/*---------------------------------------------------------------------------*/
/**
  @brief   Iteratively resample selected pixels of a pixel table into spectrum.
  @param   aPixtable  The MUSE pixel table to resample
  @param   aBinwidth  The bin width to use.
  @param   aLo        Low rejection sigma limit, use 0. to ignore low outliers
  @param   aHi        High rejection sigma limit, use 0. to ignore high outliers
  @param   aIter      The number of iterations to use
  @return  A table with a spectrum or NULL on error.

  This function carries out a linear interpolation of all selected pixels of
  a pixel table into a 1D spectrum, using muse_resampling_spectrum().

  This function uses the first version of the spectrum is checked against the
  original pixels in each spectral bin, to reject outliers outside the ranges
  spectrum - aLo * stddev and spectrum + aHi * stddev. The flagged pixel table
  is then used to compute another version of the spectrum, until aIter is
  reached. Before exiting the function, the flags are removed again from the
  original pixel table.

  Note that this works well for regions where the spectrum changes very little
  (i.e. the sky background), but it is likely to reject science content in
  areas covered by objects.

  The output spectrum is represented as a table and may not be contiguous.

  @error{set CPL_ERROR_NULL_INPUT\, return NULL,
         aPixtable or one of its components is NULL}
  @error{set CPL_ERROR_ILLEGAL_INPUT\, return NULL,
         aPixtable does not match the definition of a pixel table}
 */
/*---------------------------------------------------------------------------*/
cpl_table *
muse_resampling_spectrum_iterate(muse_pixtable *aPixtable, double aBinwidth,
                                 float aLo, float aHi, unsigned char aIter)
{
  cpl_ensure(aPixtable && aPixtable->header && aPixtable->table,
             CPL_ERROR_NULL_INPUT, NULL);
  cpl_ensure(muse_cpltable_check(aPixtable->table, muse_pixtable_def)
             == CPL_ERROR_NONE, CPL_ERROR_ILLEGAL_INPUT, NULL);

  /* initial resampling */
  cpl_table *spectrum = muse_resampling_spectrum(aPixtable, aBinwidth);
  if (aIter == 0) {
    return spectrum;
  }

  /* create easy accessors for the pixel table */
  float *lbda = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_LAMBDA),
        *data = cpl_table_get_data_float(aPixtable->table, MUSE_PIXTABLE_DATA);
  int *dq = cpl_table_get_data_int(aPixtable->table, MUSE_PIXTABLE_DQ);
  /* get row selection of the pixel table */
  cpl_array *asel = cpl_table_where_selected(aPixtable->table);
  const cpl_size *sel = cpl_array_get_data_cplsize_const(asel);
  cpl_size isel, nsel = cpl_array_get_size(asel);

  /* now iterate the spectrum creation */
  cpl_size nhi = 0, nlo = 0; /* sum up rejections of all iterations */
  unsigned char i;
  for (i = 1; i <= aIter; i++) {
    cpl_size ispec, nbins = cpl_table_get_nrow(spectrum);
    double *sdata = cpl_table_get_data_double(spectrum, "data"),
           *sstat = cpl_table_get_data_double(spectrum, "stat"),
           *sstddev = cpl_malloc(nbins * sizeof(double));
    /* compute standard deviation from the variance of the spectrum */
    for (ispec = 0; ispec < nbins; ispec++) {
      sstddev[ispec] = sqrt(sstat[ispec]);
    }

    /* loop through all selected pixel table rows  */
    for (isel = 0; isel < nsel; isel++) {
      cpl_size irow = sel[isel];
      if ((dq[irow] != EURO3D_GOODPIXEL)) {
        continue;
      }
      double l = lbda[irow];
      ispec = muse_cpltable_find_sorted(spectrum, "lambda", l);
      /* search for the right wavelength */
      if ((ispec < nbins - 1) && (sdata[ispec] < sdata[ispec + 1])) {
        ispec++;
      }
      /* flag the pixel if it's outside given high... */
      if (aHi > 0. && data[irow] > sdata[ispec] + aHi * sstddev[ispec]) {
        dq[irow] = EURO3D_OUTLIER;
        nhi++;
      }
      /* or low boundaries */
      if (aLo > 0. && data[irow] < sdata[ispec] - aLo * sstddev[ispec]) {
        dq[irow] = EURO3D_OUTLIER;
        nlo++;
      }
    } /* for isel (all selected pixel table rows) */
    cpl_free(sstddev);

    /* give some output, only in debug-level (because otherwise *
     * users would not understand where the message comes from) */
    cpl_msg_debug(__func__, "%"CPL_SIZE_FORMAT" of %"CPL_SIZE_FORMAT" pixels "
                  "are outliers (%"CPL_SIZE_FORMAT" low and %"CPL_SIZE_FORMAT
                  " high, after %hhu iteration%s)", nlo + nhi, nsel, nlo, nhi,
                  i, i == 1 ? "" : "s");

    cpl_table_delete(spectrum);
    spectrum = muse_resampling_spectrum(aPixtable, aBinwidth);
  } /* for i (all iterations) */
  cpl_array_delete(asel);

  /* reinstate the original pixel table flags */
  muse_pixtable_reset_dq(aPixtable, EURO3D_OUTLIER);

  return spectrum;
} /* muse_resampling_spectrum_iterate() */

/**@}*/
