// Copyright (c) 2018-2026  Robert J. Hijmans
//
// This file is part of the "spat" library.
//
// spat 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.
//
// spat 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 spat. If not, see <http://www.gnu.org/licenses/>.

#include "gdalwarper.h"
#include "ogr_spatialref.h"
#include "gdal_alg.h"
#include "ogrsf_frmts.h"

#include "gdal_utils.h"  // for GDALWarp() in warper_by_util

#include "spatRaster.h"
#include "string_utils.h"
#include "file_utils.h"
#include "vecmath.h"

#include "crs.h"
#include "gdalio.h"
#include "recycle.h"
#include <sstream>


namespace {
// Prefer dataset CRS; if unset, fall back to GEOLOCATION / GCP SRS from open (#1175)
std::string warp_source_crs(SpatRaster &r) {
	std::string srccrs = r.getSRS("wkt");
	if (!srccrs.empty()) return srccrs;
	std::vector<std::string> gs = r.geoloc_srs();
	for (size_t i=0; i<gs.size(); i++) {
		if (!gs[i].empty()) return gs[i];
	}
	return srccrs;
}

bool dataset_has_geolocation(GDALDatasetH hDS) {
	if (hDS == NULL) return false;
	CSLConstList md = GDALGetMetadata(hDS, "GEOLOCATION");
	if (md != NULL && CSLCount(md) > 0) return true;
	GDALRasterBandH b = GDALGetRasterBand(hDS, 1);
	if (b != NULL) {
		md = GDALGetMetadata(b, "GEOLOCATION");
		if (md != NULL && CSLCount(md) > 0) return true;
	}
	return false;
}

// netCDF GEOLOCATION lon/lat over /vsicurl/ often opens for metadata but warp
// fills the destination with NA (esp. Windows). /vsis3/ can work when the
// bucket is reachable (see #1175 CDSE example); do not refuse it here.
bool geoloc_uses_vsicurl(const SpatRasterSource &s) {
	auto has_vsicurl = [](const std::string &p) {
		return p.find("/vsicurl/") != std::string::npos;
	};
	return has_vsicurl(s.filename) || has_vsicurl(s.geoloc_x) || has_vsicurl(s.geoloc_y);
}

const char *geoloc_vsi_msg =
	"project/warp with GEOLOCATION arrays via /vsicurl/ is not supported for netCDF; download the file, or for S3 data try /vsis3/ with appropriate credentials";

bool refuse_geoloc_vsi(SpatRaster &out, const std::vector<SpatRasterSource> &sources) {
	for (size_t j=0; j<sources.size(); j++) {
		if (sources[j].has_geolocation && geoloc_uses_vsicurl(sources[j])) {
			out.setError(geoloc_vsi_msg);
			return true;
		}
	}
	return false;
}
}


/*
GDAL 3.10

std::vector<double> SpatRaster::extract_interpolate(std::vector<double> x, std::vector<double> y, std::string algo) {

	GDALRIOResampleAlg eInterpolation = GRIORA_Bilinear ;
	size_t n = x.size();
	std::vector<double> out(n, NAN);
	double value;
	GDALDatasetH hDs;

	SpatOptions opt;
	if (!open_gdal(hDs, 0, false, opt)) {
		setError("cannot open dataset");
		return(out);
	}

	GDALRasterBandH poBand = GDALGetRasterBand(hDs, 1);
	for (size_t i=0; i<n; i++) {
		if (GDALInterpolateAtPoint(pbBand, x[i], y[1],  eInterpolation, &value) == CE_None) {
			out[i] = value;
		}
	}
	GDALClose( hDs );
	return out;
}	
*/


SpatGeom getPolygonsGeom2(OGRGeometry *poGeometry) {
	SpatGeom g(polygons);
	OGRPoint ogrPt;
//	OGRwkbGeometryType geomtype = poGeometry->getGeometryType();
//	if ( geomtype == wkbPolygon ) {
		OGRPolygon *poGeom = ( OGRPolygon * )poGeometry;
		OGRLinearRing *poRing = poGeom->getExteriorRing();
		unsigned np = poRing->getNumPoints();
		std::vector<double> X(np);
		std::vector<double> Y(np);
		for (size_t i=0; i<np; i++) {
			poRing->getPoint(i, &ogrPt);
			X[i] = ogrPt.getX();
			Y[i] = ogrPt.getY();
		}
		SpatPart p(X, Y);
		unsigned nh = poGeom->getNumInteriorRings();
		for (size_t i=0; i<nh; i++) {
			OGRLinearRing *poHole = poGeom->getInteriorRing(i);
			unsigned np = poHole->getNumPoints();
			std::vector<double> X(np);
			std::vector<double> Y(np);
			for (size_t j=0; j<np; j++) {
				poHole->getPoint(j, &ogrPt);
				X[j] = ogrPt.getX();
				Y[j] = ogrPt.getY();
			}
			p.addHole(X, Y);
		}
		g.addPart(p);
//	}
	return g;
}


SpatVector SpatVector::buffer3(std::vector<double> d, unsigned quadsegs) {

	SpatVector out;
	recycle(d, size());
	GDALDataset* v = write_ogr("", "layer", "Memory", false, true, std::vector<std::string>());

	OGRLayer *poLayer = v->GetLayer(0);
	poLayer->ResetReading();

	OGRFeature *poFeature;
	while( (poFeature = poLayer->GetNextFeature()) != NULL ) {
		OGRGeometry *poGeometry = poFeature->GetGeometryRef();
		if (poGeometry != NULL) {
			// using d[0] for now
			OGRGeometry *bufGeom = poGeometry->Buffer(d[0], quadsegs);
			SpatGeom g = getPolygonsGeom2(bufGeom);
			out.addGeom(g);
		}
		OGRFeature::DestroyFeature( poFeature );
	}
	GDALClose(v);
	return out;
}


SpatVector SpatRaster::dense_extent(bool inside, bool geobounds) {

	SpatExtent e = getExtent();
	if (geobounds && is_lonlat()) {
		if ((e.ymin <= -90) || (e.ymax >= 90)) { 
			double fy = yres() / 10; // avoid Inf with Mercator
			SpatRaster g = geometry();
			e.ymin= std::max(e.ymin, -90.0+fy);
			e.ymax= std::min(e.ymax, 90.0-fy);
			double fx = xres() / 10; // avoid flipping to other side of dateline
			e.xmin= std::max(e.xmin, -180.0+fx);
			e.xmax= std::min(e.xmax, 180.0-fx);
			g.source[0].extent = e;
			return g.dense_extent(inside, false);
		}
	}

	std::vector<int64_t> rows, cols;
	if (nrow() < 51) {
		rows.resize(nrow());
		std::iota(rows.begin(), rows.end(), 0);
	} else {
		rows = seq_steps((int64_t) 0, (int64_t) nrow()-1, 50);
	}
	if (ncol() < 51) {
		cols.resize(ncol());
		std::iota(cols.begin(), cols.end(), 0);
	} else {
		cols = seq_steps((int64_t) 0, (int64_t) ncol()-1, 50);
	}

	std::vector<double> xcol = xFromCol(cols) ;
	std::vector<double> yrow = yFromRow(rows) ;

	double yr = yres() / 4;
	if (inside) {
		yrow.insert(yrow.begin(), e.ymax - yr);
		yrow.push_back(e.ymin + yr);
		std::vector<double> y0(xcol.size(), e.ymin+yr);
		std::vector<double> y1(xcol.size(), e.ymax-yr);
	} else {
		yrow.insert(yrow.begin(), e.ymax);
		yrow.push_back(e.ymin);
		std::vector<double> y0(xcol.size(), e.ymin);
		std::vector<double> y1(xcol.size(), e.ymax);
	}

	std::vector<double> y0(xcol.size(), e.ymin);
	std::vector<double> y1(xcol.size(), e.ymax);
	std::vector<double> x0(yrow.size(), e.xmin);
	std::vector<double> x1(yrow.size(), e.xmax);
	std::vector<double> x = x0;
	std::vector<double> y = yrow;
	x.insert(x.end(), xcol.begin(), xcol.end());
	y.insert(y.end(), y0.begin(), y0.end());

	std::reverse(yrow.begin(), yrow.end());
	std::reverse(xcol.begin(), xcol.end());

	x.insert(x.end(), x1.begin(), x1.end());
	y.insert(y.end(), yrow.begin(), yrow.end() );
	x.insert(x.end(), xcol.begin(), xcol.end());
	y.insert(y.end(), y1.begin(), y1.end());

	x.push_back(x[0]);
	y.push_back(y[0]);

	SpatVector v(x, y, polygons, getSRS("wkt"));

	return v;
}

#if GDAL_VERSION_MAJOR <= 2 && GDAL_VERSION_MINOR < 2

SpatRaster SpatRaster::warper(SpatRaster x, std::string crs, std::string method, bool mask, bool align, bool resample, std::string pipeline, std::vector<double> AOI, double desired_accuracy, bool allow_ballpark, double xscale, double yscale, SpatOptions &opt) {
	SpatRaster out;
	out.setError("Not supported for this old version of GDAL");
	return(out);
}


#else


bool get_output_bounds(const GDALDatasetH &hSrcDS, std::string srccrs, const std::string dstcrs, SpatRaster &r,
		bool force_geoloc=false, const std::string &geoloc_x="", const std::string &geoloc_y="") {

	if ( hSrcDS == NULL ) {
		r.setError("data source is NULL");
		return false;
	}

	// Get Source coordinate system.
	// const char *pszSrcWKT = GDALGetProjectionRef( hSrcDS );
	const char *pszSrcWKT = srccrs.c_str();
	if ( pszSrcWKT == NULL || strlen(pszSrcWKT) == 0 ) {
		r.setError("data source has no WKT");
		return false;
	}

	OGRSpatialReference* oSRS = new OGRSpatialReference;
	std::string msg = "";
	if (is_ogr_error(oSRS->SetFromUserInput( dstcrs.c_str() ), msg)) {
		r.setError(msg);
		delete oSRS;
		return false;
	};

	char *pszDstWKT = NULL;
#if GDAL_VERSION_MAJOR >= 3
	const char *options[3] = { "MULTILINE=YES", "FORMAT=WKT2", NULL };
	oSRS->exportToWkt( &pszDstWKT, options);
#else
	oSRS->exportToWkt( &pszDstWKT );
#endif
 	delete oSRS;

	// Ensure GEOLOCATION is visible on the dataset used for SuggestedWarpOutput.
	if (force_geoloc && !geoloc_x.empty() && !geoloc_y.empty() && !dataset_has_geolocation(hSrcDS)) {
		GDALSetMetadataItem(hSrcDS, "SRS", pszSrcWKT, "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "X_DATASET", geoloc_x.c_str(), "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "Y_DATASET", geoloc_y.c_str(), "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "X_BAND", "1", "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "Y_BAND", "1", "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "PIXEL_OFFSET", "0", "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "PIXEL_STEP", "1", "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "LINE_OFFSET", "0", "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "LINE_STEP", "1", "GEOLOCATION");
		GDALSetMetadataItem(hSrcDS, "GEOREFERENCING_CONVENTION", "PIXEL_CENTER", "GEOLOCATION");
	}

	// Swath / geolocation arrays: force GEOLOC_ARRAY so GDAL does not treat
	// pixel indices as map coordinates when suggesting the output grid (#1175).
	char **papszTO = nullptr;
	papszTO = CSLSetNameValue(papszTO, "SRC_SRS", pszSrcWKT);
	papszTO = CSLSetNameValue(papszTO, "DST_SRS", pszDstWKT);
	const bool use_geoloc = force_geoloc || dataset_has_geolocation(hSrcDS);
	if (use_geoloc) {
		const char *xd = GDALGetMetadataItem(hSrcDS, "X_DATASET", "GEOLOCATION");
		const char *yd = GDALGetMetadataItem(hSrcDS, "Y_DATASET", "GEOLOCATION");
		std::string xds = xd ? xd : geoloc_x;
		std::string yds = yd ? yd : geoloc_y;
		if (!xds.empty() && !yds.empty()) {
			gdal_capture_messages_begin();
			GDALDatasetH hx = GDALOpen(xds.c_str(), GA_ReadOnly);
			GDALDatasetH hy = (hx != NULL) ? GDALOpen(yds.c_str(), GA_ReadOnly) : NULL;
			gdal_capture_messages_end(false);
			if (hx == NULL || hy == NULL) {
				if (hx) GDALClose(hx);
				if (hy) GDALClose(hy);
				CSLDestroy(papszTO);
				CPLFree(pszDstWKT);
				r.setError(geoloc_vsi_msg);
				return false;
			}
			GDALClose(hx);
			GDALClose(hy);
		}
		papszTO = CSLSetNameValue(papszTO, "SRC_METHOD", "GEOLOC_ARRAY");
	}
	void *hTransformArg = GDALCreateGenImgProjTransformer2(hSrcDS, NULL, papszTO);
	CSLDestroy(papszTO);
	if (hTransformArg == NULL ) {
		r.setError("cannot create TranformArg");
		CPLFree(pszDstWKT);
		return false;
	}
	CPLFree(pszDstWKT);

	double adfDstGeoTransform[6];
	int nPixels=0, nLines=0;
	CPLErr eErr = GDALSuggestedWarpOutput( hSrcDS, GDALGenImgProjTransform,
					hTransformArg, adfDstGeoTransform, &nPixels, &nLines );

	GDALDestroyGenImgProjTransformer( hTransformArg );
	if ( eErr != CE_None ) {
		r.setError("cannot create warp output");
		return false;
	}


	r.source[0].ncol = nPixels;
	r.source[0].nrow = nLines;

	r.source[0].extent.xmin = adfDstGeoTransform[0]; /* left x */
	/* w-e pixel resolution */
	r.source[0].extent.xmax = r.source[0].extent.xmin + adfDstGeoTransform[1] * nPixels;
	r.source[0].extent.ymax = adfDstGeoTransform[3]; // top y
	r.source[0].extent.ymin = r.source[0].extent.ymax + nLines * adfDstGeoTransform[5];

	r.setSRS({dstcrs});

	return true;
}

/*
	// Create output with same datatype as first input band.
	GDALDataType eDT = GDALGetRasterDataType(GDALGetRasterBand(hSrcDS,1));
	GDALDataType eDT;
	getGDALDataType(datatype, eDT);

	// Create the output DS.

	GDALDriverH hDriver = GDALGetDriverByName( driver.c_str() );
	if ( hDriver == NULL ) {
		msg = "empty driver";
		return false;
	}
	if (driver == "MEM") {
		hDstDS = GDALCreate( hDriver, "", nPixels, nLines, nlyrs, eDT, NULL );
	} else {
		hDstDS = GDALCreate( hDriver, filename.c_str(), nPixels, nLines, nlyrs, eDT, NULL );
	}
	if ( hDstDS == NULL ) {
		msg = "cannot create output dataset";
		return false;
	}

	// Write out the projection definition.
	GDALSetProjection( hDstDS, pszDstWKT );
	GDALSetGeoTransform( hDstDS, adfDstGeoTransform );

	// Copy the color table, if required.
	GDALColorTableH hCT;
	hCT = GDALGetRasterColorTable( GDALGetRasterBand(hSrcDS,1) );
	if( hCT != NULL )
		GDALSetRasterColorTable( GDALGetRasterBand(hDstDS,1), hCT );

	CPLFree(pszDstWKT);
 	delete oSRS;

	return true;
}
*/

bool getAlgo(GDALResampleAlg &alg, std::string m) {

	if (m=="sum") {
#if GDAL_VERSION_MAJOR >= 3 && GDAL_VERSION_MINOR >= 1
		alg = GRA_Sum;
		return true;
	}
#else
		return false;
	}
#endif

	if (m=="rms") {
#if GDAL_VERSION_MAJOR >= 3 && GDAL_VERSION_MINOR >= 3
		alg = GRA_RMS;
		return true;
	}
#else
		return false;
	}
#endif


	if ( m == "near" ) {
		alg = GRA_NearestNeighbour;
	} else if (m=="bilinear") {
		alg = GRA_Bilinear;
	} else if (m=="cubic") {
		alg = GRA_Cubic;
	} else if (m=="cubicspline") {
		alg = GRA_CubicSpline;
	} else if (m=="lanczos") {
		alg = GRA_Lanczos;
	} else if (m=="average") {
		alg = GRA_Average;
	} else if (m=="mode") {
		alg = GRA_Mode;
	} else if (m=="max") {
		alg = GRA_Max;
	} else if (m=="min") {
		alg = GRA_Min;
	} else if (m=="median") {
		alg = GRA_Med;
	} else if (m=="q1") {
		alg = GRA_Q1;
	} else if (m=="q3") {
		alg = GRA_Q3;
	} else {
		return false;
	}
	return true;
}


bool is_valid_warp_method(const std::string &method) {
	std::vector<std::string> m { "near", "bilinear", "cubic", "cubicspline", "lanczos", "average", "mode", "max", "min", "median", "q1", "q3", "sum", "rms"};
	return (std::find(m.begin(), m.end(), method) != m.end());
}


bool set_warp_options(GDALWarpOptions *psWarpOptions, GDALDatasetH &hSrcDS, GDALDatasetH &hDstDS, std::vector<size_t> srcbands, std::vector<size_t> dstbands, std::string method, std::string srccrs, std::string msg, bool verbose, unsigned threads, std::string pipeline="", std::vector<double> AOI=std::vector<double>(), double desired_accuracy=-1.0, bool allow_ballpark=true, double xscale=0, double yscale=0) {

	if (srcbands.size() != dstbands.size()) {
		msg = "number of source bands must match number of dest bands";
		return false;
	}
	int nbands = srcbands.size();

	GDALResampleAlg a;
	if (!getAlgo(a, method)) {
		msg = method + " is not a valid method";
		return false;
	}

    // Setup warp options.
    psWarpOptions->hSrcDS = hSrcDS;
    psWarpOptions->hDstDS = hDstDS;
	psWarpOptions->eResampleAlg = a;
    psWarpOptions->nBandCount = nbands;

	// Set warp memory limit high enough to avoid GDAL sub-chunking.
	// GDAL should process each block in one pass. The limit is a ceiling,
	// not a pre-allocation; GDAL only allocates what it actually needs.
	double dst_cells = (double)GDALGetRasterXSize(hDstDS)
		                  * (double)GDALGetRasterYSize(hDstDS);
	double mem_needed = dst_cells * (double)nbands * 8.0 * 3.0;
	psWarpOptions->dfWarpMemoryLimit = std::max(mem_needed, 64.0 * 1024.0 * 1024.0);

    psWarpOptions->panSrcBands = (int *) CPLMalloc(sizeof(int) * nbands );
    psWarpOptions->panDstBands = (int *) CPLMalloc(sizeof(int) * nbands );
	psWarpOptions->padfSrcNoDataReal = (double *) CPLMalloc(sizeof(double) * nbands );
	psWarpOptions->padfDstNoDataReal = (double *) CPLMalloc(sizeof(double) * nbands );
	psWarpOptions->padfSrcNoDataImag = (double *) CPLMalloc(sizeof(double) * nbands );
	psWarpOptions->padfDstNoDataImag = (double *) CPLMalloc(sizeof(double) * nbands );

	GDALRasterBandH hBand;
	int hasNA;
	for (int i=0; i<nbands; i++) {
		psWarpOptions->panSrcBands[i] = (int) srcbands[i]+1;
		psWarpOptions->panDstBands[i] = (int) dstbands[i]+1;

		hBand = GDALGetRasterBand(hSrcDS, srcbands[i]+1);
		double naflag = GDALGetRasterNoDataValue(hBand, &hasNA);
		if (hasNA) {
			psWarpOptions->padfSrcNoDataReal[i] = naflag;
			psWarpOptions->padfDstNoDataReal[i] = naflag;
			hBand = GDALGetRasterBand(hDstDS, dstbands[i]+1);
			GDALSetRasterNoDataValue(hBand, naflag);
		} else {
			psWarpOptions->padfSrcNoDataReal[i] = NAN;
			psWarpOptions->padfDstNoDataReal[i] = NAN;
		}
		psWarpOptions->padfSrcNoDataImag[i] = 0;
		psWarpOptions->padfDstNoDataImag[i] = 0;
    }

	//psWarpOptions->pfnProgress = GDALTermProgress;

	psWarpOptions->papszWarpOptions =
     CSLSetNameValue( psWarpOptions->papszWarpOptions, "INIT_DEST", "NO_DATA");
	psWarpOptions->papszWarpOptions =
      CSLSetNameValue( psWarpOptions->papszWarpOptions, "WRITE_FLUSH", "YES");

	if (threads > 0) {
		// forward the actual thread cap rather than ALL_CPUS, so that
		// terraOptions(threads=n) is honored by the warper
		psWarpOptions->papszWarpOptions =
			CSLSetNameValue( psWarpOptions->papszWarpOptions, "NUM_THREADS", std::to_string(threads).c_str());
	}

	if (xscale > 0) {
		psWarpOptions->papszWarpOptions =
			CSLSetNameValue(psWarpOptions->papszWarpOptions, "XSCALE",
				std::to_string(xscale).c_str());
	}
	if (yscale > 0) {
		psWarpOptions->papszWarpOptions =
			CSLSetNameValue(psWarpOptions->papszWarpOptions, "YSCALE",
				std::to_string(yscale).c_str());
	}

	char **papszTO = nullptr;
#if GDAL_VERSION_NUM >= 3000000
	if (!pipeline.empty()) {
		papszTO = CSLSetNameValue(papszTO, "COORDINATE_OPERATION", pipeline.c_str());
	}
	if (AOI.size() == 4) {
		std::string aoi_str = std::to_string(AOI[0]) + "," +
			std::to_string(AOI[1]) + "," +	std::to_string(AOI[2]) + "," + std::to_string(AOI[3]);
		papszTO = CSLSetNameValue(papszTO, "AREA_OF_INTEREST", aoi_str.c_str());
	}
#if GDAL_VERSION_NUM >= 3030000
	if (desired_accuracy >= 0) {
		papszTO = CSLSetNameValue(papszTO, "DESIRED_ACCURACY",
			std::to_string(desired_accuracy).c_str());
	}
	if (!allow_ballpark) {
		papszTO = CSLSetNameValue(papszTO, "ALLOW_BALLPARK", "NO");
	}
#endif
#endif
	if (dataset_has_geolocation(hSrcDS)) {
		papszTO = CSLSetNameValue(papszTO, "SRC_METHOD", "GEOLOC_ARRAY");
	}
	if (papszTO != nullptr) {
		if (pipeline.empty()) {
			papszTO = CSLSetNameValue(papszTO, "SRC_SRS", srccrs.c_str());
			papszTO = CSLSetNameValue(papszTO, "DST_SRS", GDALGetProjectionRef(hDstDS));
		}
		psWarpOptions->pTransformerArg =
			GDALCreateGenImgProjTransformer2(hSrcDS, hDstDS, papszTO);
	} else {
		psWarpOptions->pTransformerArg =
			GDALCreateGenImgProjTransformer(hSrcDS, srccrs.c_str(),
				hDstDS, GDALGetProjectionRef(hDstDS), FALSE, 0.0, 1);
	}
	CSLDestroy(papszTO);
	if (psWarpOptions->pTransformerArg == NULL) {
		msg = "failed to create coordinate transformer";
		return false;
	}
	psWarpOptions->pfnTransformer = GDALGenImgProjTransform;

	return true;
}

/*
bool gdal_warper(GDALWarpOptions *psWarpOptions, GDALDatasetH &hSrcDS, GDALDatasetH &hDstDS) {
    GDALWarpOperation oOperation;
    if (oOperation.Initialize( psWarpOptions ) != CE_None) {
		return false;
	}
    if (oOperation.ChunkAndWarpImage(0, 0, GDALGetRasterXSize(hDstDS), GDALGetRasterYSize(hDstDS)) != CE_None) {
		return false;
	}
	return true;
}
*/



SpatRaster SpatRaster::warper(SpatRaster x, std::string crs, std::string method, bool mask, bool align, bool resample, std::string pipeline, std::vector<double> AOI, double desired_accuracy, bool allow_ballpark, double xscale, double yscale, SpatOptions &opt) {

	size_t ns = nsrc();
	bool fixext = false;
	for (size_t j=0; j<ns; j++) {
		// GEOLOCATION swaths must keep their file handle: materializing to
		// memory/temp drops the GEOLOCATION domain and breaks project().
		if (source[j].has_geolocation) continue;
		if ((source[j].extset || source[j].flipped) && (!source[j].memory) && (!source[j].rotated)) {
			fixext = true;
			break;
		}
	}

	if (fixext) {
		SpatRaster r = *this;
		SpatOptions xopt(opt);
		xopt.ncopies = std::max((size_t) 10, xopt.ncopies*2);
		for (size_t j=0; j<ns; j++) {
			if (source[j].has_geolocation) continue;
			if ((source[j].extset || source[j].flipped) && (!r.source[j].memory) && (!source[j].rotated)) {
				SpatRaster tmp(source[j]);	
				if (tmp.canProcessInMemory(xopt)) {
					tmp.readAll();
				} else {
					tmp = tmp.writeTempRaster(opt);
				}
				r.source[j] = tmp.source[0]; 
			}
		}
		return r.warper(x, crs, method, mask, align, resample, pipeline, AOI, desired_accuracy, allow_ballpark, xscale, yscale, opt);
	}


	// apply_so uses readStart(), which refuses rotated sources; GDAL warp reads
	// the file directly and applies per-band scale/offset as appropriate.
	bool any_rotated = false;
	for (size_t j = 0; j < ns; j++) {
		if (source[j].rotated) {
			any_rotated = true;
			break;
		}
	}
	if (hasScaleOffset() && !any_rotated) {
		SpatOptions opt2(opt);
		SpatRaster app = apply_so(opt2);	
		return app.warper(x, crs, method, mask, align, resample, pipeline, AOI, desired_accuracy, allow_ballpark, xscale, yscale, opt);
	}

	SpatRaster out = x.geometry(nlyr(), false, false);
	ProjNoiseScope _pns(out.msg);
	if (!is_valid_warp_method(method)) {
		out.setError("not a valid warp method");
		return out;
	}
	std::string srccrs = warp_source_crs(*this);
	if (resample) {
		out.setSRS(srccrs);
	}
	if (refuse_geoloc_vsi(out, source)) {
		return out;
	}

	out.setNames(getNames());
	if ((method == "near") || (method == "mode")) {
		out.source[0].hasColors = hasColors();
		out.source[0].setCols(getColors());
		out.source[0].hasCategories = hasCategories();
		out.source[0].setCats(getCategories());
		out.rgb = rgb;
		out.rgblyrs = rgblyrs;
		out.rgbtype = rgbtype;
	}
	if (hasTime()) {
		out.source[0].hasTime = true;
		out.source[0].timestep = getTimeStep();
		out.source[0].timezone = getTimeZone();
		out.source[0].time = getTime();
	}
	if (hasUnit()) {
		out.source[0].hasUnit = true;
		out.source[0].unit = getUnit();
	}

	bool use_crs = !crs.empty();
	if (use_crs) {
		align = false;
		resample = false;
	} else if (!hasValues()) {
		std::string fname = opt.get_filename();
		if (!fname.empty()) {
			out.addWarning("raster has no values, not writing to file");
		}
		return out;
	}
	if (align) {
		crs = out.getSRS("wkt");
	}

	if (!resample) {
		if (srccrs.empty()) {
			out.setError("input raster CRS not set (and no GEOLOCATION/GCP SRS found)");
			return out;
		}
	}

	lrtrim(crs);
	SpatOptions sopt(opt);
	if (use_crs || align) {
		GDALDatasetH hSrcDS;
		if (!open_gdal(hSrcDS, 0, false, sopt)) {
			out.setError("cannot create dataset from source");
			return out;
		}
		out.setSRS(crs);
		const bool force_geoloc = (!source.empty() && source[0].has_geolocation);
		const std::string gx = force_geoloc ? source[0].geoloc_x : "";
		const std::string gy = force_geoloc ? source[0].geoloc_y : "";
		if (!get_output_bounds(hSrcDS, srccrs, crs, out, force_geoloc, gx, gy)) {
			GDALClose( hSrcDS );
			if (!out.hasError()) {
				out.setError("cannot get output boundaries for the target crs");
			}
			return out;
		}
		GDALClose( hSrcDS );
	} else if (!resample) {
		bool use_pipe = !pipeline.empty();
		OGRSpatialReference source_srs, target_srs;
		const char *pszDefFrom = srccrs.c_str();
		OGRErr erro = source_srs.SetFromUserInput(pszDefFrom);
		if (erro != OGRERR_NONE) {
			out.setError("input crs is not valid");
			return out;
		}
		if (!use_pipe) {
			std::string targetcrs = out.getSRS("wkt");
			const char *pszDefTo = targetcrs.c_str();
			erro = target_srs.SetFromUserInput(pszDefTo);
			if (erro != OGRERR_NONE) {
				out.setError("output crs is not valid");
				return out;
			}
		}
		OGRCoordinateTransformation *poCT;
#if GDAL_VERSION_NUM >= 3000000
		OGRCoordinateTransformationOptions ct_opts;
		bool use_ct_opts = false;
		if (use_pipe) {
			if (!ct_opts.SetCoordinateOperation(pipeline.c_str(), false)) {
				out.setError("pipeline not accepted");
				return out;
			}
			use_ct_opts = true;
		}
		if (AOI.size() == 4) {
			if (!ct_opts.SetAreaOfInterest(AOI[0], AOI[1], AOI[2], AOI[3])) {
				out.setError("area of interest not accepted");
				return out;
			}
			use_ct_opts = true;
		}
#if GDAL_VERSION_NUM >= 3030000
		if (desired_accuracy >= 0) {
			ct_opts.SetDesiredAccuracy(desired_accuracy);
			use_ct_opts = true;
		}
		if (!allow_ballpark) {
			ct_opts.SetBallparkAllowed(false);
			use_ct_opts = true;
		}
#endif
		OGRSpatialReference *pTarget = use_pipe ? nullptr : &target_srs;
		if (use_ct_opts) {
			poCT = OGRCreateCoordinateTransformation(&source_srs, pTarget, ct_opts);
		} else {
			poCT = OGRCreateCoordinateTransformation(&source_srs, &target_srs);
		}
#else
		if (use_pipe || AOI.size() == 4) {
			out.setError("pipeline and AOI require GDAL >= 3");
			return out;
		}
		poCT = OGRCreateCoordinateTransformation(&source_srs, &target_srs);
#endif
		if( poCT == NULL )	{
			out.setError( "Cannot do this transformation" );
			return(out);
		}
		OCTDestroyCoordinateTransformation(poCT);
	}

	if (align) {
		SpatExtent e = out.getExtent();
		e = x.align(e, "out");
		out.setExtent(e, false, true, "");
		std::vector<double> res = x.resolution();
		out = out.setResolution(res[0], res[1]);
	}
	if (!hasValues()) {
		return out;
	}

	SpatOptions mopt;
	if (mask) {
		mopt = opt;
		opt = SpatOptions(opt);
	}

	opt.ncopies += 4;
	if (!out.writeStart(opt, filenames())) {
		return out;
	}

	std::string errmsg;
	SpatExtent eout = out.getExtent();

	std::vector<bool> has_so(nlyr(), false);
	std::vector<double> scale(nlyr(), 1);
	std::vector<double> offset(nlyr(), 0);


	double halfy = out.yres() / 2;
	for (size_t i = 0; i < out.bs.n; i++) {
		eout.ymax = out.yFromRow(out.bs.row[i]) + halfy;
		eout.ymin = out.yFromRow(out.bs.row[i] + out.bs.nrows[i]-1) - halfy;
		SpatRaster crop_out = out.crop(eout, "near", false, sopt);
		GDALDatasetH hDstDS;

		if (!crop_out.create_gdalDS(hDstDS, "", "MEM", false, NAN, has_so, scale, offset, sopt)) {
			return crop_out;
		}

		int bandstart = 0;
		for (size_t j=0; j<ns; j++) {
			GDALDatasetH hSrcDS;
			if (!open_gdal(hSrcDS, j, false, sopt)) {
				out.setError("cannot create dataset from source");
				if( hDstDS != NULL ) GDALClose( (GDALDatasetH) hDstDS );
				return out;
			}
			std::vector<size_t> srcbands = source[j].layers;
			std::vector<size_t> dstbands(srcbands.size());
			std::iota (dstbands.begin(), dstbands.end(), bandstart);
			bandstart += dstbands.size();

			GDALWarpOptions *psWarpOptions = GDALCreateWarpOptions();
			if (!set_warp_options(psWarpOptions, hSrcDS, hDstDS, srcbands, dstbands, method, srccrs, errmsg, opt.get_verbose(), opt.threads, pipeline, AOI, desired_accuracy, allow_ballpark, xscale, yscale)) {
				if (hSrcDS != NULL) GDALClose((GDALDatasetH) hSrcDS);
				if (hDstDS != NULL) GDALClose((GDALDatasetH) hDstDS);
				GDALDestroyWarpOptions(psWarpOptions);
				out.setError(errmsg);
				return out;
			}
			//ok = gdal_warper(psWarpOptions, hSrcDS, hDstDS);

			bool ok=true;
			GDALWarpOperation oOperation;
			if (oOperation.Initialize(psWarpOptions) != CE_None) {
				ok = false;
			} else if (oOperation.ChunkAndWarpImage(0, 0, GDALGetRasterXSize(hDstDS), GDALGetRasterYSize(hDstDS)) != CE_None) {
				ok = false;
			}
			GDALDestroyGenImgProjTransformer(psWarpOptions->pTransformerArg);
			GDALDestroyWarpOptions(psWarpOptions);

			if (hSrcDS != NULL) GDALClose((GDALDatasetH) hSrcDS);
			if (!ok) {
				if (hDstDS != NULL) GDALClose((GDALDatasetH) hDstDS);
				out.setError("warp failure");
				return out;
			}
		}

		bool ok = crop_out.from_gdalMEM(hDstDS, false, true);
		if (hDstDS != NULL) GDALClose((GDALDatasetH) hDstDS);
		if (!ok) {
			out.setError("cannot do this transformation (warp)");
			return out;
		}
//		std::vector<double> v = crop_out.getValues(-1, opt);
//		if (!out.writeBlock(v, i)) return out;
		if (!out.writeBlock(crop_out.source[0].values, i)) return out;

	}
	out.writeStop();

	if (mask) {
		SpatVector v = dense_extent(true, true);
		v = v.project(out.getSRS("wkt"), true);
		if (v.nrow() > 0) {
			out = out.mask(v, false, NAN, true, mopt);
		} else {
			out.addWarning("masking failed");
		}
	}
	return out;
}



/*
SpatRaster SpatRaster::oldwarper(SpatRaster x, std::string crs, std::string method, bool mask, bool align, bool resample, SpatOptions &opt) {

	size_t ns = nsrc();
	bool fixext = false;
	for (size_t j=0; j<ns; j++) {
		if (source[j].extset && (!source[j].memory)) {
			fixext = true;
			break;
		}
	}
	if (fixext) {
		SpatRaster r = *this;
		for (size_t j=0; j<ns; j++) {
			if (r.source[j].extset && (!r.source[j].memory)) {
				SpatRaster tmp(source[j]);
				//if (tmp.canProcessInMemory(opt)) {
				//	tmp.readAll();
				//} else {
				tmp = tmp.writeTempRaster(opt);
				r.source[j] = tmp.source[0]; 
			}
		}
		return r.warper(x, crs, method, mask, align, resample, opt);
	}


	SpatRaster out = x.geometry(nlyr(), false, false);
	if (!is_valid_warp_method(method)) {
		out.setError("not a valid warp method");
		return out;
	}
	std::string srccrs = warp_source_crs(*this);
	if (resample) {
		out.setSRS(srccrs);
	}
	if (refuse_geoloc_vsi(out, source)) {
		return out;
	}

	out.setNames(getNames());
	if (method == "near") {
		out.source[0].hasColors = hasColors();
		out.source[0].setCols(getColors());
		out.source[0].hasCategories = hasCategories();
		out.source[0].setCats(getCategories());
		out.rgb = rgb;
		out.rgblyrs = rgblyrs;
		out.rgbtype = rgbtype;
	}
	if (hasTime()) {
		out.source[0].hasTime = true;
		out.source[0].timestep = getTimeStep();
		out.source[0].timezone = getTimeZone();
		out.source[0].time = getTime();
	}
	if (hasUnit()) {
		out.source[0].hasUnit = true;
		out.source[0].unit = getUnit();
	}

	bool use_crs = !crs.empty();
	if (use_crs) {
		align = false;
		resample = false;
	} else if (!hasValues()) {
		std::string fname = opt.get_filename();
		if (!fname.empty()) {
			out.addWarning("raster has no values, not writing to file");
		}
		return out;
	}
	if (align) {
		crs = out.getSRS("wkt");
	}

	if (!resample) {
		if (srccrs.empty()) {
			out.setError("input raster CRS not set (and no GEOLOCATION/GCP SRS found)");
			return out;
		}
	}

	lrtrim(crs);
	SpatOptions sopt(opt);
	if (use_crs || align) {
		GDALDatasetH hSrcDS;
		SpatRaster g = geometry(1);
		if (!g.open_gdal(hSrcDS, 0, false, sopt)) {
			out.setError("cannot create dataset from source");
			return out;
		}
		out.setSRS(crs);
		const bool force_geoloc = (!source.empty() && source[0].has_geolocation);
		const std::string gx = force_geoloc ? source[0].geoloc_x : "";
		const std::string gy = force_geoloc ? source[0].geoloc_y : "";
		if (!get_output_bounds(hSrcDS, srccrs, crs, out, force_geoloc, gx, gy)) {
			GDALClose( hSrcDS );
			if (!out.hasError()) {
				out.setError("cannot get output boundaries");
			}
			return out;
		}
		GDALClose( hSrcDS );
	} else if (!resample) {
		OGRSpatialReference source, target;
		const char *pszDefFrom = srccrs.c_str();
		OGRErr erro = source.SetFromUserInput(pszDefFrom);
		if (erro != OGRERR_NONE) {
			out.setError("input crs is not valid");
			return out;
		}
		std::string targetcrs = out.getSRS("wkt");
		const char *pszDefTo = targetcrs.c_str();
		erro = target.SetFromUserInput(pszDefTo);
		if (erro != OGRERR_NONE) {
			out.setError("output crs is not valid");
			return out;
		}
		OGRCoordinateTransformation *poCT;
		poCT = OGRCreateCoordinateTransformation(&source, &target);
		if( poCT == NULL )	{
			out.setError( "Cannot do this transformation" );
			return(out);
		}
		OCTDestroyCoordinateTransformation(poCT);
	}

	if (align) {
		SpatExtent e = out.getExtent();
		e = x.align(e, "out");
		out.setExtent(e, false, true, "");
		std::vector<double> res = x.resolution();
		out = out.setResolution(res[0], res[1]);
	}
	if (!hasValues()) {
		return out;
	}

	SpatOptions mopt;
	if (mask) {
		mopt = opt;
		opt = SpatOptions(opt);
	}

	opt.ncopies += 4;
	if (!out.writeStart(opt, filenames())) {
		return out;
	}

	std::string errmsg;
	SpatExtent eout = out.getExtent();


	std::vector<bool> has_so = source[0].has_scale_offset;
	std::vector<double> scale = source[0].scale;
	std::vector<double> offset = source[0].offset;
	for (size_t i=1; i<ns; i++) {
		has_so.insert(has_so.end(), source[0].has_scale_offset.begin(), source[0].has_scale_offset.end());
		scale.insert(scale.end(), source[0].scale.begin(), source[0].scale.end());
		offset.insert(offset.end(), source[0].offset.begin(), source[0].offset.end());
	}

	double halfy = out.yres() / 2;
	for (size_t i = 0; i < out.bs.n; i++) {
		eout.ymax = out.yFromRow(out.bs.row[i]) + halfy;
		eout.ymin = out.yFromRow(out.bs.row[i] + out.bs.nrows[i]-1) - halfy;
		SpatRaster crop_out = out.crop(eout, "near", false, sopt);
		GDALDatasetH hDstDS;

		if (!crop_out.create_gdalDS(hDstDS, "", "MEM", false, NAN, has_so, scale, offset, sopt)) {
			return crop_out;
		}

		int bandstart = 0;
		for (size_t j=0; j<ns; j++) {
			GDALDatasetH hSrcDS;
			if (!open_gdal(hSrcDS, j, false, sopt)) {
				out.setError("cannot create dataset from source");
				if( hDstDS != NULL ) GDALClose( (GDALDatasetH) hDstDS );
				return out;
			}
			std::vector<size_t> srcbands = source[j].layers;
			std::vector<size_t> dstbands(srcbands.size());
			std::iota (dstbands.begin(), dstbands.end(), bandstart);
			bandstart += dstbands.size();

			GDALWarpOptions *psWarpOptions = GDALCreateWarpOptions();
			bool ok = set_warp_options(psWarpOptions, hSrcDS, hDstDS, srcbands, dstbands, method, srccrs, errmsg, opt.get_verbose(), opt.threads);
			if (!ok) {
				if( hDstDS != NULL ) GDALClose( (GDALDatasetH) hDstDS );
				out.setError(errmsg);
				return out;
			}
			//ok = gdal_warper(psWarpOptions, hSrcDS, hDstDS);
			GDALWarpOperation oOperation;
			if (oOperation.Initialize( psWarpOptions ) != CE_None) {
				ok = false;
			} else if (oOperation.ChunkAndWarpImage(0, 0, GDALGetRasterXSize(hDstDS), GDALGetRasterYSize(hDstDS)) != CE_None) {
				ok = false;
			}
			GDALDestroyGenImgProjTransformer( psWarpOptions->pTransformerArg );
			GDALDestroyWarpOptions( psWarpOptions );

			if( hSrcDS != NULL ) GDALClose( (GDALDatasetH) hSrcDS );
			if (!ok) {
				if( hDstDS != NULL ) GDALClose( (GDALDatasetH) hDstDS );
				out.setError("warp failure");
				return out;
			}
		}


		bool ok = crop_out.from_gdalMEM(hDstDS, false, true);
		if( hDstDS != NULL ) GDALClose( (GDALDatasetH) hDstDS );
		if (!ok) {
			out.setError("cannot do this transformation (warp)");
			return out;
		}
//		std::vector<double> v = crop_out.getValues(-1, opt);
//		if (!out.writeBlock(v, i)) return out;
		if (!out.writeBlock(crop_out.source[0].values, i)) return out;

	}
	out.writeStop();
	if (mask) {
		SpatVector v = dense_extent(true, true);
		v = v.project(out.getSRS("wkt"), true);
		if (v.nrow() > 0) {
			out = out.mask(v, false, NAN, true, mopt);
		} else {
			out.addWarning("masking failed");
		}
	}
	return out;
}

*/


SpatRaster SpatRaster::warper_by_util(SpatRaster x, std::string crs, std::string method, bool mask, bool align, bool resample, std::string pipeline, std::vector<double> AOI, double desired_accuracy, bool allow_ballpark, double xscale, double yscale, SpatOptions &opt) {

	size_t ns = nsrc();
	bool fixext = false;
	for (size_t j=0; j<ns; j++) {
		if ((source[j].extset || source[j].flipped) && (!source[j].memory) && (!source[j].rotated)) {
			fixext = true;
			break;
		}
	}
	if (fixext) {
		SpatRaster r = *this;
		for (size_t j=0; j<ns; j++) {
			if ((r.source[j].extset || r.source[j].flipped) && (!r.source[j].memory) && (!r.source[j].rotated)) {
				SpatRaster tmp(source[j]);
				//if (tmp.canProcessInMemory(opt)) {
				//	tmp.readAll();
				//} else {
				tmp = tmp.writeTempRaster(opt);
				r.source[j] = tmp.source[0]; 
			}
		}
		return r.warper_by_util(x, crs, method, mask, align, resample, pipeline, AOI, desired_accuracy, allow_ballpark, xscale, yscale, opt);
	}

	// GDAL warp reads raw values; apply scale/offset first (like warper()), else
	// scaled sources (including multidim) would be warped on their raw values.
	bool any_rotated = false;
	for (size_t j = 0; j < ns; j++) {
		if (source[j].rotated) {
			any_rotated = true;
			break;
		}
	}
	if (hasScaleOffset() && !any_rotated) {
		SpatOptions opt2(opt);
		SpatRaster app = apply_so(opt2);
		return app.warper_by_util(x, crs, method, mask, align, resample, pipeline, AOI, desired_accuracy, allow_ballpark, xscale, yscale, opt);
	}

	SpatRaster out = x.geometry(nlyr(), false, false);
	ProjNoiseScope _pns(out.msg);
	if (!is_valid_warp_method(method)) {
		out.setError("not a valid warp method");
		return out;
	}
	std::string srccrs = warp_source_crs(*this);
	if (resample) {
		out.setSRS(srccrs);
	}
	if (refuse_geoloc_vsi(out, source)) {
		return out;
	}

	out.setNames(getNames());
	if ((method == "near") || (method == "mode")) {
		out.source[0].hasColors = hasColors();
		out.source[0].setCols(getColors());
		out.source[0].hasCategories = hasCategories();
		out.source[0].setCats(getCategories());
		out.rgb = rgb;
		out.rgblyrs = rgblyrs;
		out.rgbtype = rgbtype;
	}
	if (hasTime()) {
		out.source[0].hasTime = true;
		out.source[0].timestep = getTimeStep();
		out.source[0].timezone = getTimeZone();
		out.source[0].time = getTime();
	}

	bool use_crs = !crs.empty();
	if (use_crs) {
		align = false;
		resample = false;
	} else if (!hasValues()) {
		std::string fname = opt.get_filename();
		if (!fname.empty()) {
			out.addWarning("raster has no values, not writing to file");
		}
		return out;
	}
	if (align) {
		crs = out.getSRS("wkt");
	}

	if (!resample) {
		if (srccrs.empty()) {
			out.setError("input raster CRS not set (and no GEOLOCATION/GCP SRS found)");
			return out;
		}
	}

	lrtrim(crs);
	SpatOptions sopt(opt);
	if (use_crs || align) {
		GDALDatasetH hSrcDS;
		if (!open_gdal(hSrcDS, 0, false, sopt)) {
			out.setError("cannot create dataset from source");
			return out;
		}
		out.setSRS(crs);
		const bool force_geoloc = (!source.empty() && source[0].has_geolocation);
		const std::string gx = force_geoloc ? source[0].geoloc_x : "";
		const std::string gy = force_geoloc ? source[0].geoloc_y : "";
		if (!get_output_bounds(hSrcDS, srccrs, crs, out, force_geoloc, gx, gy)) {
			GDALClose( hSrcDS );
			if (!out.hasError()) {
				out.setError("cannot get output boundaries for the target crs");
			}
			return out;
		}
		GDALClose( hSrcDS );
	} else if (!resample) {
		bool use_pipe = !pipeline.empty();
		OGRSpatialReference source_srs, target_srs;
		const char *pszDefFrom = srccrs.c_str();
		OGRErr erro = source_srs.SetFromUserInput(pszDefFrom);
		if (erro != OGRERR_NONE) {
			out.setError("input crs is not valid");
			return out;
		}
		if (!use_pipe) {
			std::string targetcrs = out.getSRS("wkt");
			const char *pszDefTo = targetcrs.c_str();
			erro = target_srs.SetFromUserInput(pszDefTo);
			if (erro != OGRERR_NONE) {
				out.setError("output crs is not valid");
				return out;
			}
		}
		OGRCoordinateTransformation *poCT;
#if GDAL_VERSION_NUM >= 3000000
		OGRCoordinateTransformationOptions ct_opts;
		bool use_ct_opts = false;
		if (use_pipe) {
			if (!ct_opts.SetCoordinateOperation(pipeline.c_str(), false)) {
				out.setError("pipeline not accepted");
				return out;
			}
			use_ct_opts = true;
		}
		if (AOI.size() == 4) {
			if (!ct_opts.SetAreaOfInterest(AOI[0], AOI[1], AOI[2], AOI[3])) {
				out.setError("area of interest not accepted");
				return out;
			}
			use_ct_opts = true;
		}
#if GDAL_VERSION_NUM >= 3030000
		if (desired_accuracy >= 0) {
			ct_opts.SetDesiredAccuracy(desired_accuracy);
			use_ct_opts = true;
		}
		if (!allow_ballpark) {
			ct_opts.SetBallparkAllowed(false);
			use_ct_opts = true;
		}
#endif
		OGRSpatialReference *pTarget = use_pipe ? nullptr : &target_srs;
		if (use_ct_opts) {
			poCT = OGRCreateCoordinateTransformation(&source_srs, pTarget, ct_opts);
		} else {
			poCT = OGRCreateCoordinateTransformation(&source_srs, &target_srs);
		}
#else
		if (use_pipe || AOI.size() == 4) {
			out.setError("pipeline and AOI require GDAL >= 3");
			return out;
		}
		poCT = OGRCreateCoordinateTransformation(&source_srs, &target_srs);
#endif
		if( poCT == NULL )	{
			out.setError( "Cannot do this transformation" );
			return(out);
		}
		OCTDestroyCoordinateTransformation(poCT);
	}

	if (align) {
		SpatExtent e = out.getExtent();
		e = x.align(e, "out");
		out.setExtent(e, false, true, "");
		std::vector<double> res = x.resolution();
		out = out.setResolution(res[0], res[1]);
	}
	if (!hasValues()) {
		return out;
	}

	SpatOptions mopt;
	if (mask) {
		mopt = opt;
		opt = SpatOptions(opt);
	}

	opt.ncopies += 7;
	if (!out.writeStart(opt, filenames())) {
		return out;
	}

	std::string errmsg;
	SpatExtent eout = out.getExtent();


	std::vector<bool> has_so = source[0].has_scale_offset;
	std::vector<double> scale = source[0].scale;
	std::vector<double> offset = source[0].offset;
	for (size_t i=1; i<ns; i++) {
		has_so.insert(has_so.end(), source[0].has_scale_offset.begin(), source[0].has_scale_offset.end());
		scale.insert(scale.end(), source[0].scale.begin(), source[0].scale.end());
		offset.insert(offset.end(), source[0].offset.begin(), source[0].offset.end());
	}

	double halfy = out.yres() / 2;
	for (size_t i = 0; i < out.bs.n; i++) {
		int bandstart = 0;
		eout.ymax = out.yFromRow(out.bs.row[i]) + halfy;
		eout.ymin = out.yFromRow(out.bs.row[i] + out.bs.nrows[i]-1) - halfy;
		SpatRaster crop_out = out.crop(eout, "near", false, sopt);
		GDALDatasetH hDstDS;
		GDALDatasetH hWarpedDS = NULL;
		if (!crop_out.create_gdalDS(hDstDS, "", "MEM", false, NAN, has_so, scale, offset, sopt)) {
			return crop_out;
		}

		for (size_t j=0; j<ns; j++) {
			GDALDatasetH hSrcDS;
			if (!open_gdal(hSrcDS, j, false, sopt)) {
				out.setError("cannot create dataset from source");
				if( hDstDS != NULL ) GDALClose( (GDALDatasetH) hDstDS );
				return out;
			}
			std::vector<size_t> srcbands = source[j].layers;
			std::vector<size_t> dstbands(srcbands.size());
			std::iota (dstbands.begin(), dstbands.end(), bandstart);
			bandstart += dstbands.size();

			bool ok = true; 
			if (srcbands.size() != dstbands.size()) {
				errmsg = "number of source bands must match number of dest bands";
				ok =  false;
			}
//			int nbands = srcbands.size();

			GDALResampleAlg a;
			if (!getAlgo(a, method)) {
				ok = false; 
				if ((method=="sum") || (method=="rms")) {
					errmsg = method + " not available in your version of GDAL";
				} else {
					errmsg = "unknown resampling algorithm";
				}
			}
			if (!ok) {
				if( hDstDS != NULL ) GDALClose( (GDALDatasetH) hDstDS );
				out.setError(errmsg);
				return out;
			}
			// Compute warp memory limit to avoid GDAL sub-chunking artifacts
			double dst_pix = (double)GDALGetRasterXSize(hDstDS)
			               * (double)GDALGetRasterYSize(hDstDS);
			double wm_bytes = dst_pix * (double)srcbands.size() * 8.0 * 3.0;
			wm_bytes = std::max(wm_bytes, 64.0 * 1024.0 * 1024.0);
			int wm_mb = (int)std::ceil(wm_bytes / (1024.0 * 1024.0));
			std::string wm_str = std::to_string(wm_mb);

			std::vector<std::string> warp_args = {"-r", method, "-wm", wm_str};
			std::vector<const char*> cargs;
			for (auto &s : warp_args) cargs.push_back(s.c_str());
			cargs.push_back(nullptr);
			GDALWarpAppOptions *psWarpAppOptions = GDALWarpAppOptionsNew(
				const_cast<char**>(cargs.data()), nullptr);
			GDALWarpAppOptionsSetProgress(psWarpAppOptions, NULL, NULL );
			GDALWarpAppOptionsSetWarpOption(psWarpAppOptions, "INIT_DEST", "NO_DATA"); 
			GDALWarpAppOptionsSetWarpOption(psWarpAppOptions, "WRITE_FLUSH", "YES"); 
			if (opt.threads > 0) {
				GDALWarpAppOptionsSetWarpOption(psWarpAppOptions, "NUM_THREADS", std::to_string(opt.threads).c_str());
			}
			if (xscale > 0) {
				GDALWarpAppOptionsSetWarpOption(psWarpAppOptions, "XSCALE",
					std::to_string(xscale).c_str());
			}
			if (yscale > 0) {
				GDALWarpAppOptionsSetWarpOption(psWarpAppOptions, "YSCALE",
					std::to_string(yscale).c_str());
			}
			//--------------------------------------------------------------------------

			hWarpedDS = GDALWarp("", hDstDS, 1 , &hSrcDS, psWarpAppOptions, 0);
			GDALWarpAppOptionsFree(psWarpAppOptions); 
			if( hSrcDS != NULL ) GDALClose( (GDALDatasetH) hSrcDS );

		}

		bool ok = crop_out.from_gdalMEM(hWarpedDS, false, true);
		if( hWarpedDS != NULL ) GDALClose( (GDALDatasetH) hWarpedDS );
		if (!ok) {
			out.setError("cannot do this transformation (warp)");
			return out;
		}
		std::vector<double> v = crop_out.getValues(-1, opt);
		if (!out.writeBlock(v, i)) return out;
	}
	out.writeStop();
	if (mask) {
		SpatVector v = dense_extent(true, true);
		v = v.project(out.getSRS("wkt"), true);
		if (v.nrow() > 0) {
			out = out.mask(v, false, NAN, true, mopt);
		} else {
			out.addWarning("masking failed");
		}
	}
	return out;
}


std::vector<double> SpatRaster::warp_scale(SpatRaster x, size_t n) {
	// Samples an n x n grid of points in the source raster, projects them
	// to the destination CRS, and returns quantiles of the local
	// resampling ratio (destination pixels per source pixel) along each
	// axis. Used to choose XSCALE/YSCALE warp options that are stable
	// across processing chunks.
	//
	// Return is a 10-element vector:
	//   [0..4] xscale: min, q1, median, q3, max
	//   [5..9] yscale: min, q1, median, q3, max

	std::vector<double> result(10, NAN);

	std::string src_crs = warp_source_crs(*this);
	std::string dst_crs = x.getSRS("wkt");
	if (src_crs.empty()) {
		setError("source raster CRS not set");
		return result;
	}
	if (dst_crs.empty()) {
		setError("destination raster CRS not set");
		return result;
	}

	std::vector<double> src_res = resolution();
	std::vector<double> dst_res = x.resolution();
	SpatExtent e = getExtent();

	if (n < 3) n = 3;
	if (n > ncol()) n = ncol();
	if (n > nrow()) n = nrow();
	if (n < 3) {
		setError("source raster is too small to sample");
		return result;
	}

	std::vector<double> xs(n), ys(n);
	double xlo = e.xmin + src_res[0];
	double xhi = e.xmax - src_res[0];
	double ylo = e.ymin + src_res[1];
	double yhi = e.ymax - src_res[1];
	for (size_t i = 0; i < n; i++) {
		xs[i] = xlo + (xhi - xlo) * (double)i / (double)(n - 1);
		ys[i] = ylo + (yhi - ylo) * (double)i / (double)(n - 1);
	}

	size_t N = n * n;
	std::vector<double> px0(N), py0(N);
	std::vector<double> pxdx(N), pydx(N);
	std::vector<double> pxdy(N), pydy(N);
	size_t k = 0;
	for (size_t j = 0; j < n; j++) {
		for (size_t i = 0; i < n; i++) {
			px0[k]  = xs[i];              py0[k]  = ys[j];
			pxdx[k] = xs[i] + src_res[0]; pydx[k] = ys[j];
			pxdy[k] = xs[i];              pydy[k] = ys[j] + src_res[1];
			k++;
		}
	}

	SpatMessages m;
	m = transform_coordinates(px0,  py0,  src_crs, dst_crs);
	if (m.has_error) { setError("coordinate transformation failed"); return result; }
	m = transform_coordinates(pxdx, pydx, src_crs, dst_crs);
	if (m.has_error) { setError("coordinate transformation failed"); return result; }
	m = transform_coordinates(pxdy, pydy, src_crs, dst_crs);
	if (m.has_error) { setError("coordinate transformation failed"); return result; }

	std::vector<double> xsc, ysc;
	xsc.reserve(N);
	ysc.reserve(N);
	for (size_t i = 0; i < N; i++) {
		double dx = std::fabs(pxdx[i] - px0[i]);
		double dy = std::fabs(pydy[i] - py0[i]);
		if (std::isfinite(dx) && dx > 0 && dst_res[0] > 0) {
			xsc.push_back(dx / dst_res[0]);
		}
		if (std::isfinite(dy) && dy > 0 && dst_res[1] > 0) {
			ysc.push_back(dy / dst_res[1]);
		}
	}

	if (xsc.empty() || ysc.empty()) {
		setError("no valid projected points to compute scale");
		return result;
	}

	std::vector<double> probs = {0.0, 0.25, 0.50, 0.75, 1.0};
	std::vector<double> xq = vquantile(xsc, probs, true);
	std::vector<double> yq = vquantile(ysc, probs, true);

	for (size_t i = 0; i < 5; i++) result[i]     = xq[i];
	for (size_t i = 0; i < 5; i++) result[i + 5] = yq[i];
	return result;
}


#endif  // GDAL_VERSION_MAJOR <= 2 && GDAL_VERSION_MINOR < 2


SpatRaster SpatRaster::resample(SpatRaster x, std::string method, bool mask, bool agg, SpatOptions &opt) {

	size_t nl = nlyr();
	SpatRaster out = x.geometry(nl);
	out.setNames(getNames());

	out.setNames(getNames());
	std::vector<std::string> f {"bilinear", "near"};
	if (std::find(f.begin(), f.end(), method) == f.end()) {
		out.setError("unknown warp method");
		return out;
	}
	if (!hasValues()) {
		return out;
	}

	std::string crsin = source[0].srs.wkt;
	std::string crsout = out.source[0].srs.wkt;
	bool do_prj = true;
	if ((crsin == crsout) || crsin.empty() || crsout.empty()) {
		do_prj = false;
	}

	if (!do_prj) {
		SpatExtent e = out.getExtent();
		e = e.intersect(getExtent());
		if (!e.valid()) {
			out.addWarning("No spatial overlap");
			return out;
		}
	}

	if (agg) {
		if (do_prj) {
			// compare changes in true cell areas
			// if (some) output cells are much larger than input, we could
			//    a) disaggregate "x", warp, and aggregate the results
			//    b) or give a warning?
		} else {
			size_t xq = x.xres() / xres();
			size_t yq = x.yres() / yres();
			if (std::max(xq, yq) > 1) {
				xq = xq == 0 ? 1 : xq;
				yq = yq == 0 ? 1 : yq;
				std::vector<size_t> agf = {yq, xq, 1};
				SpatOptions agopt(opt);
				SpatRaster xx;
				if (method == "bilinear") {
					xx = aggregate(agf, "mean", true, agopt);
				} else {
					xx = aggregate(agf, "modal", true, agopt);
				}
				return xx.resample(x, method, mask, false, opt);
			}
		}
	}

	SpatOptions mopt;
	if (mask) {
		mopt = opt;
		opt = SpatOptions(opt);
	}

	size_t nc = out.ncol();
  	if (!out.writeStart(opt, filenames())) { return out; }
	for (size_t i = 0; i < out.bs.n; i++) {
        size_t firstcell = out.cellFromRowCol(out.bs.row[i], 0);
		size_t lastcell  = out.cellFromRowCol(out.bs.row[i]+out.bs.nrows[i]-1, nc-1);
		std::vector<double> cells(1+lastcell-firstcell);
		std::iota (std::begin(cells), std::end(cells), firstcell);
        std::vector<std::vector<double>> xy = out.xyFromCell(cells);

		if (do_prj) {
			#ifdef useGDAL
			out.msg = transform_coordinates(xy[0], xy[1], crsout, crsin);
			#else
			out.setError("GDAL is needed for crs transformation, but not available");
			return out;
			#endif
		}
		std::vector<std::vector<double>> e = extractXY(xy[0], xy[1], method, false, opt);
		std::vector<double> v = flatten(e);
		if (!out.writeValues(v, out.bs.row[i], out.bs.nrows[i])) return out;
	}
	out.writeStop();


	if (mask) {
		SpatVector v = dense_extent(true, true);
		v = v.project(out.getSRS("wkt"), true);
		if (v.nrow() > 0) {
			out = out.mask(v, false, NAN, true, mopt);
		} else {
			out.addWarning("masking failed");
		}
	}


	return(out);
}



// When GDAL exposes a GEOLOCATION domain (MODIS/VIIRS swath), the geotransform
// is only approximate: for full output (unless aoi is provided) pad extent then trim NA edges.
// Other rotated rasters (e.g. JPEG) usually have no GEOLOCATION — no pad or trim.

static bool has_geolocation(GDALDatasetH hDS) {
	return dataset_has_geolocation(hDS);
}

static SpatExtent expand_extent(SpatExtent e) {
	static const double k = 1;
	//if (k <= 0) return e;
	double wx = e.xmax - e.xmin;
	double wy = e.ymax - e.ymin;
	double mx = wx * k * 0.5;
	double my = wy * k * 0.5;
	return SpatExtent(e.xmin - mx, e.xmax + mx, e.ymin - my, e.ymax + my);
}


bool GCP_geotrans(GDALDataset *poDataset, double* adfGeoTransform) {
	int n = poDataset->GetGCPCount();
	if (n == 0) return false;
	const GDAL_GCP *gcp;
	gcp	= poDataset->GetGCPs();
	return GDALGCPsToGeoTransform(n, gcp, adfGeoTransform, true);
}

//#include <filesystem>

SpatRaster SpatRaster::rectify(std::string method, SpatRaster aoi, unsigned useaoi, bool snap, SpatOptions &opt) {
	SpatRaster out = geometry();

	if (nsrc() > 1) {
		out.setError("you can rectify only one data source at a time");
		return(out);
	}
	if (!source[0].rotated) {
		out.setError("this source is not rotated");
		return(out);
	}
	GDALDataset *poDataset = NULL;
#if GDAL_VERSION_NUM >= 3040000
	// A multidim source stores a bare file name that cannot be opened as a
	// classic raster (no bands/geotransform); expose the array as a classic dataset instead.
	if (source[0].is_multidim) {
		GDALDatasetH hMD = NULL;
		if (open_gdal_multidim(hMD, 0)) {
			poDataset = (GDALDataset *) hMD;
		}
	}
#endif
	if (poDataset == NULL) {
		poDataset = openGDAL(source[0].filename, GDAL_OF_RASTER | GDAL_OF_READONLY, source[0].open_drivers, source[0].open_ops);
	}

	if( poDataset == NULL )  {
		setError("cannot read from " + source[0].filename);
		return out;
	}
	double gt[6];
	if( poDataset->GetGeoTransform(gt) != CE_None ) {
		if (GCP_geotrans(poDataset, gt)) {
			poDataset->SetGeoTransform(gt);
		} else {
			out.setError("can't get the geotransform");
			GDALClose( (GDALDatasetH) poDataset );
			return out;
		}
	}

	const bool pad_extent = has_geolocation((GDALDatasetH) poDataset);
	const bool swath_pad_trim = pad_extent && (useaoi == 0);

	// use bounds suggested by GDALWarp.
#if GDAL_VERSION_MAJOR > 2 || (GDAL_VERSION_MAJOR == 2 && GDAL_VERSION_MINOR >= 2)
	std::string srccrs = warp_source_crs(*this);
	if (!get_output_bounds((GDALDatasetH) poDataset, srccrs, srccrs, out)) {
		GDALClose( (GDALDatasetH) poDataset );
		if (!out.hasError()) {
			out.setError("cannot get rectified output extent");
		}
		return out;
	}
	GDALClose( (GDALDatasetH) poDataset );
	if (swath_pad_trim) {
		SpatExtent ee = expand_extent(out.getExtent());
		out.setExtent(ee, true, true, "out");
	}
#else
	GDALClose( (GDALDatasetH) poDataset );
	double nc = ncol();
	double nr = nrow();
	std::vector<double> px = {0, 0, nc, nc};
	std::vector<double> py = {0, nr, 0, nr};
	std::vector<double> xx(4);
	std::vector<double> yy(4);
	for (size_t i=0; i<4; i++) {
		xx[i] = gt[0] + px[i]*gt[1] + py[i]*gt[2];
		yy[i] = gt[3] + px[i]*gt[4] + py[i]*gt[5];
	}
	SpatExtent estext(vmin(xx, TRUE), vmax(xx, TRUE), vmin(yy, TRUE), vmax(yy, TRUE));
	if (swath_pad_trim) {
		estext = expand_extent(estext);
	}
	out = out.setResolution(fabs(gt[1]), fabs(gt[5]));
	out.setExtent(estext, true, true, "out");
#endif

	if (useaoi == 1) { // use extent
		SpatExtent en = aoi.getExtent();
		if (snap) {
			en = out.align(en, "near");
			out.setExtent(en, false, true, "near");
		} else {
			out.setExtent(en, false, true, "");
		}
	} else if (useaoi == 2){  // extent and resolution
		out = aoi.geometry();
	} // else { // if (useaoi == 0) // no aoi

	//e = out.getExtent();

	SpatOptions wopt(opt);
	out = warper(out, "", method, false, false, true, wopt);

	if (rgb) {
		out.rgb=true;
		out.rgbtype = rgbtype;
		out.rgblyrs = rgblyrs;
	}

	if (swath_pad_trim) {
		SpatRaster trm = out.trim2(NAN, 0, opt);
		if (!trm.hasError()) {
			return trm;
		}
	}

	std::string filename = opt.get_filename();
	if (filename != "") {
		out = out.writeRaster(opt);
	}

	return(out);
}



SpatVector SpatRaster::polygonize(bool round, bool values, bool narm, bool aggregate, int digits, SpatOptions &opt) {

	SpatVector out;
	out.srs = source[0].srs;
	SpatOptions topt(opt);

	SpatRaster tmp;
	std::string warn = "";
	if (nlyr() > 1) {
		warn = "only the first layer is polygonized when 'dissolve=TRUE'";
		tmp = subset({0}, topt);
	} else {
		tmp = *this;
	}

//	bool usemask = false;
	SpatRaster mask;
	if (narm) {
//		usemask = true;
		SpatOptions mopt(topt);
		mopt.set_datatype("INT1U");
		mask = tmp.isfinite(false, mopt);
	} 


	if (round && (digits > 0)) {
		tmp = tmp.math2("round", digits, topt);
		round = false;
	} else if (tmp.source[0].extset || tmp.source[0].flipped) { 
		tmp = tmp.hardCopy(topt);
	}

/*
	} else if (tmp.sources_from_file()) {
		// for NAN and INT files. Should have a check for that
		//tmp = tmp.arith(0, "+", false, topt);
		// riskier
		tmp.readAll();
	}
*/


	GDALDatasetH rstDS;
	if (!tmp.open_gdal(rstDS, 0, false, topt)) {
		out.setError("cannot open dataset");
		return out;
	}

    GDALDataset *srcDS=NULL;

#if GDAL_VERSION_MAJOR <= 2 && GDAL_VERSION_MINOR <= 2
	srcDS = (GDALDataset *) rstDS;
#else
	srcDS = srcDS->FromHandle(rstDS);
#endif

	GDALDataset *maskDS=NULL;
	GDALDatasetH rstMask;
	if (narm) {
		if (!mask.open_gdal(rstMask, 0, false, opt)) {
			out.setError("cannot open dataset");
			return out;
		}
#if GDAL_VERSION_MAJOR <= 2 && GDAL_VERSION_MINOR <= 2
		maskDS = (GDALDataset *) rstMask;
#else
		maskDS = srcDS->FromHandle(rstMask);
#endif
	}

    GDALDataset *poDS = NULL;

#if (GDAL_VERSION_MAJOR == 3 && GDAL_VERSION_MINOR >= 11) || (GDAL_VERSION_MAJOR >= 4)	
    GDALDriver *poDriver = GetGDALDriverManager()->GetDriverByName( "MEM" );
#else
    GDALDriver *poDriver = GetGDALDriverManager()->GetDriverByName( "Memory" );
#endif

    if( poDriver == NULL )  {
        out.setError( "cannot create output driver");
        return out;
    }
    poDS = poDriver->Create("", 0, 0, 0, GDT_Unknown, NULL );
    if( poDS == NULL ) {
        out.setError("Creation of output dataset failed" );
        return out;
    }
	std::vector<std::string> nms = getNames();
	std::string name = nms[0];

	OGRSpatialReference *SRS = NULL;

    OGRLayer *poLayer;
    poLayer = poDS->CreateLayer(name.c_str(), SRS, wkbPolygon, NULL );
    if( poLayer == NULL ) {
        out.setError( "Layer creation failed" );
        return out;
    }
	if (SRS != NULL) SRS->Release();

	OGRFieldDefn oField(name.c_str(), round ? OFTInteger : OFTReal);
	if( poLayer->CreateField( &oField ) != OGRERR_NONE ) {
		out.setError( "Creating field failed");
		return out;
	}

	GDALRasterBand  *poBand;
	poBand = srcDS->GetRasterBand(tmp.source[0].layers[0] + 1);

	//int hasNA=1;
	//double naflag = poBand->GetNoDataValue(&hasNA);

	CPLErr err;
	if (narm) {
		GDALRasterBand *maskBand;
		maskBand = maskDS->GetRasterBand(1);
		if (round) {
			err = GDALPolygonize(poBand, maskBand, poLayer, 0, NULL, NULL, NULL);
		} else {
			err = GDALFPolygonize(poBand, maskBand, poLayer, 0, NULL, NULL, NULL);
		}
		GDALClose(maskDS);
	} else {
		if (round) {
			err = GDALPolygonize(poBand, NULL, poLayer, 0, NULL, NULL, NULL);
		} else {
			err = GDALFPolygonize(poBand, NULL, poLayer, 0, NULL, NULL, NULL);
		}
	}
	if (err == 4) {
		out.setError("polygonize error");
		return out;
	}
	GDALClose(srcDS);

	std::vector<double> fext;
	SpatVector fvct;
	out.read_ogr(poDS, "", "", fext, fvct, false, "", "");
	GDALClose(poDS);

	if (aggregate && (out.nrow() > 0)) {
		out = out.aggregate(name, false);
	}

	if (!values) {
		out.df = SpatDataFrame();
	}
	if (warn != "") {
		out.addWarning(warn);
	}

	return out;
}



SpatRaster SpatRaster::rgb2col(size_t r,  size_t g, size_t b, SpatOptions &opt) {

	SpatRaster out = geometry(1);

	if (!hasValues()) {
		out.setError("input raster has no values");
		return out;
	}	

	if (nlyr() < 3) {
		out.setError("need at least three layers");
		return out;
	}
	size_t mxlyr = std::max(std::max(r, g), b);
	if (nlyr() < mxlyr) {
		out.setError("layer number for R, G, B, cannot exceed nlyr()");
		return out;
	}

	if (hasScaleOffset()) {
		SpatOptions opt2(opt);
		SpatRaster app = apply_so(opt2);
		app.rgb2col(r, g, b, opt);
	}

	std::vector<size_t> lyrs = {r, g, b};
	SpatOptions ops(opt);
	SpatRaster tmp = subset(lyrs, ops);

	if (source[0].extset || source[0].flipped) {
		SpatOptions topt(opt);
		tmp = tmp.hardCopy(topt);
		return tmp.rgb2col(r, g, b, opt);
	} else {
		tmp = tmp.collapse_sources();
	}

	std::string filename = opt.get_filename();
	opt.set_datatype("INT1U");
	std::string driver;
	if (filename.empty()) {
		if (canProcessInMemory(opt)) {
			driver = "MEM";
		} else {
			filename = tempFile(opt.get_tempdir(), opt.tmpfile, ".tif");
			opt.set_filenames({filename});
			driver = "GTiff";
		}
	} else {
		driver = opt.get_filetype();
		getGDALdriver(filename, driver);
		if (driver.empty()) {
			out.setError("cannot guess file type from filename");
			return out;
		}

		std::string errmsg;
		if (!can_write({filename}, filenames(), opt.get_overwrite(), errmsg)) {
			out.setError(errmsg);
			return out;
		}
	}

	GDALDatasetH hSrcDS, hDstDS;
	if (!tmp.open_gdal(hSrcDS, 0, false, ops)) {
		out.setError("cannot create dataset from source");
		return out;
	}
	GDALRasterBandH R = GDALGetRasterBand(hSrcDS,1);
	GDALRasterBandH G = GDALGetRasterBand(hSrcDS,1);
	GDALRasterBandH B = GDALGetRasterBand(hSrcDS,1);

	GDALColorTableH hColorTable= GDALCreateColorTable(GPI_RGB);

	if (GDALComputeMedianCutPCT(R, G, B, NULL, 256, hColorTable, NULL, NULL) != CE_None) {
		out.setError("cannot create color table");
		GDALClose(hSrcDS);
		return out;
	}

	if (!out.create_gdalDS(hDstDS, filename, driver, true, 0, {false}, {0.0}, {1.0}, opt)) {
		out.setError("cannot create new dataset");
		GDALClose(hSrcDS);
		return out;
	}

	GDALRasterBandH hTarget = GDALGetRasterBand(hDstDS, 1);
	GDALSetRasterColorInterpretation(hTarget, GCI_PaletteIndex);
	if (GDALDitherRGB2PCT(R, G, B, hTarget, hColorTable, NULL, NULL) != CE_None) {
		out.setError("cannot set color table");
		GDALClose(hSrcDS);
		GDALClose(hDstDS);
		return out;
	}
	GDALClose(hSrcDS);

	if (driver == "MEM") {
		if (!out.from_gdalMEM(hDstDS, false, true)) {
			out.setError("conversion failed (mem)");
			GDALClose(hDstDS);
			return out;
		}
		SpatDataFrame cdf;
		cdf.add_column(1, "red");
		cdf.add_column(1, "green");
		cdf.add_column(1, "blue");
		cdf.add_column(1, "alpha");
		size_t nc = GDALGetColorEntryCount(hColorTable);
		cdf.reserve(nc);

		for (size_t i=0; i<nc; i++) {
			const GDALColorEntry* col = GDALGetColorEntry(hColorTable, i);
			cdf.iv[0].push_back(col->c1);
			cdf.iv[1].push_back(col->c2);
			cdf.iv[2].push_back(col->c3);
			cdf.iv[3].push_back(col->c4);
		}
		out.source[0].hasColors.resize(1);
		out.source[0].setCol(0, cdf);
	} else {
		if (GDALSetRasterColorTable(hTarget, hColorTable) != CE_None) {
			out.setError("cannot set color table");
			GDALClose(hDstDS);
			return out;
		}
	}
	GDALClose(hDstDS);
	if (driver != "MEM") {
		out = SpatRaster(filename, {-1}, {""}, {}, {}, false, false, {});
	}
	return out;
}





#if GDAL_VERSION_MAJOR >= 3 && GDAL_VERSION_MINOR >= 1

SpatRaster SpatRaster::viewshed(const std::vector<double> obs, const std::vector<double> vals, const double curvcoef, const int mode, const double maxdist, const int heightmode, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if (could_be_lonlat()) {
		out.setError("the method does not support lon/lat data");
		return out;
	}

	if (!hasValues()) {
		out.setError("input raster has no values");
		return out;
	}

	GDALViewshedOutputType outmode;
	if (heightmode==1) {
		outmode = GVOT_NORMAL; //= heightmode;
	} else if (heightmode==2) {
		outmode = GVOT_MIN_TARGET_HEIGHT_FROM_DEM;
	} else if (heightmode==3) {
		outmode = GVOT_MIN_TARGET_HEIGHT_FROM_GROUND;
	} else {
		out.setError("invalid output type");
		return out;
	}


	GDALViewshedMode emode;
	if (mode==1) {
		emode = GVM_Diagonal;
	} else if (mode==2) {
		emode = GVM_Edge;
	} else if (mode==3) {
		emode = GVM_Max;
	} else if (mode==4) {
		emode = GVM_Min;
	} else {
		out.setError("invalid mode");
		return out;
	}

	double minval = -9999;
	if (source[0].getHasRange(0)) {
		minval = source[0].getRangeMin(0) - 9999;
	}
	SpatOptions topt(opt);

	SpatRaster x;
	if (nlyr() > 1) {
		out.addWarning("viewshed is only done for the first layer");
		x = subset({0}, topt);
		x = x.replaceValues({NAN}, {minval}, 0, false, NAN, false, topt);
	} else {
		x = replaceValues({NAN}, {minval}, 0, false, NAN, false, topt);
	}

	std::string fname = opt.get_filename();
	std::string driver;
	if (!fname.empty()) {
		driver = opt.get_filetype();
		getGDALdriver(fname, driver);
		if (driver.empty()) {
			setError("cannot guess file type from filename");
			return out;
		}
		std::string errmsg;
		if (!can_write({fname}, filenames(), opt.get_overwrite(), errmsg)) {
			out.setError(errmsg);
			return out;
		}
	}

	std::string filename = tempFile(topt.get_tempdir(), topt.tmpfile, ".tif");
	driver = "GTiff";

	GDALDatasetH hSrcDS;
	if (!x.open_gdal(hSrcDS, 0, false, topt)) {
		out.setError("cannot open input dataset");
		return out;
	}

	GDALDriverH hDriver = GDALGetDriverByName( driver.c_str() );
	if ( hDriver == NULL ) {
		out.setError("empty driver");
		return out;
	}

	GIntBig diskNeeded = ncell() * 4;
	char **papszOptions = set_GDAL_options(driver, diskNeeded, false, topt.parallel, topt.threads, topt.gdal_options);

	GDALRasterBandH hSrcBand = GDALGetRasterBand(hSrcDS, 1);

	GDALDatasetH hDstDS = GDALViewshedGenerate(hSrcBand, driver.c_str(), filename.c_str(), papszOptions, obs[0], obs[1], obs[2], obs[3], vals[0], vals[1], vals[2], vals[3], curvcoef, emode, maxdist, NULL, NULL, outmode, NULL);

	if (hDstDS != NULL) {
		GDALClose(hDstDS);
		GDALClose(hSrcDS);
		CSLDestroy( papszOptions );
		out = SpatRaster(filename, {-1}, {""}, {}, {}, false, false, {});
	} else {
		GDALClose(hSrcDS);
		CSLDestroy( papszOptions );
		out.setError("something went wrong");
	}
	if (heightmode==1) {
		out.setValueType(3);
		out.setNames({"viewshed"});
	} else if (heightmode==2) {
		out.setNames({"above_sea"});
	} else {
		out.setNames({"above_land"});
	}
	out = out.mask(*this, false, NAN, NAN, opt);
	return out;
}

#else


SpatRaster SpatRaster::viewshed(const std::vector<double> obs, const std::vector<double> vals, const double curvcoef, const int mode, const double maxdist, const int heightmode, SpatOptions &opt) {
	SpatRaster out;
	out.setError("viewshed is not available for your version of GDAL. Need 3.1 or higher");
	return out;
}


#endif

std::string doubleToAlmostChar(double value){
    std::stringstream ss ;
    ss << value;
	std::string out = ss.str();
	return out;
}

SpatRaster SpatRaster::proximity(double target, double exclude, bool keepNA, std::string unit, bool buffer, double maxdist, bool remove_zero, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if (nlyr() > 1) {
		out.addWarning("only the first layer is processed");
	}

	if (!hasValues()) {
		out.setError("input raster has no values");
		return out;
	}	

	if (hasScaleOffset()) {
		SpatOptions opt2(opt);
		SpatRaster app = apply_so(opt2);
		return app.proximity(target, exclude, keepNA, unit, buffer, maxdist, remove_zero, opt);
	}

	if (source[0].extset || source[0].flipped) { 
		SpatOptions topt(opt);
		SpatRaster etmp;
		if (nlyr() > 1) {
			etmp = etmp.subset({0}, topt);
			etmp = etmp.hardCopy(topt);
		} else {
			etmp = hardCopy(topt);
		}
		return etmp.proximity(target, exclude, keepNA, unit, buffer, maxdist, remove_zero, opt);
	}

	// GDAL computes distances in CRS units. 
	double m = 1;
	if (!buffer) {
		if (!source[0].srs.m_dist(m, is_lonlat(), unit)) {
			out.setError("invalid distance unit. Should be 'm' or 'km'");
			return out;
		}
	}
	bool scale = (m != 1);

	std::string filename = opt.get_filename();
	std::string driver;
	if (filename.empty()) {
		if (canProcessInMemory(opt)) {
			driver = "MEM";
		} else {
			filename = tempFile(opt.get_tempdir(), opt.tmpfile, ".tif");
			opt.set_filenames({filename});
			driver = "GTiff";
		}
	} else {
		driver = opt.get_filetype();
		getGDALdriver(filename, driver);
		if (driver.empty()) {
			setError("cannot guess file type from filename");
			return out;
		}
		std::string errmsg;
		if (!can_write({filename}, filenames(), opt.get_overwrite(), errmsg)) {
			out.setError(errmsg);
			return out;
		}
	}

	// GDAL proximity algo fails with other drivers? See #1116
//	driver = "MEM";

	GDALDatasetH hSrcDS, hDstDS;

	GDALDriverH hDriver = GDALGetDriverByName( driver.c_str() );
	if ( hDriver == NULL ) {
		out.setError("empty driver");
		return out;
	}

	GIntBig diskNeeded = ncell() * 4;
	char **papszOptions = set_GDAL_options(driver, diskNeeded, false, opt.parallel, opt.threads, opt.gdal_options);
	papszOptions = CSLSetNameValue(papszOptions, "DISTUNITS", "GEO");

	SpatOptions ops(opt);
	SpatRaster x;
	bool mask = false;
	std::vector<double> mvals;
	if ((!buffer) && (maxdist > 0)) {
		// maxdist is expressed in "unit"; GDAL expects CRS units
		papszOptions = CSLSetNameValue(papszOptions, "MAXDIST", doubleToAlmostChar(maxdist / m).c_str());
	}

	if (buffer) {
		if (remove_zero) {
			x = isnotnan(true, ops);
		}		
		papszOptions = CSLSetNameValue(papszOptions, "MAXDIST", doubleToAlmostChar(maxdist).c_str());
		papszOptions = CSLSetNameValue(papszOptions, "FIXED_BUF_VAL", doubleToAlmostChar(1.0).c_str());
	} else if (std::isnan(target)) {
		if (std::isnan(exclude)) { // no exclusions
			x = isnotnan(false, ops);
		} else { // exclusion becomes target and is masked later
			x = replaceValues({exclude, NAN}, {0, 0}, 1, true, 1, false, ops);
			mvals.push_back(exclude);
			mask = true;
		}
	} else {
		//option for keepNA does not work, perhaps because of int conversion
		//papszOptions = CSLSetNameValue(papszOptions, "USE_INPUT_NODATA", "YES");

		if (std::isnan(exclude)) {
			if (keepNA) {
				x = replaceValues({target, NAN}, {0, 0}, 1, true, 1, false, ops);
				mvals.push_back(exclude);
				mask = true;
			} else {
				x = replaceValues({target}, {0}, 1, true, 1, false, ops);
			}
		} else {
			x = replaceValues({exclude, target}, {0, 0}, 1, true, 1, false, ops);
			mvals.push_back(exclude);
			mask = true;
		}
	}
	if (x.hasValues()) {
		if (!x.open_gdal(hSrcDS, 0, false, ops)) {
			out.setError("cannot open input dataset");
			return out;
		}
	} else if (!open_gdal(hSrcDS, 0, false, ops)) {
		out.setError("cannot open input dataset");
		return out;
	}

	std::string tmpfile = tempFile(opt.get_tempdir(), opt.tmpfile, ".tif");
	std::string fname = (mask || scale) ? tmpfile : filename;
	if (!out.create_gdalDS(hDstDS, fname, driver, false, 0, {false}, {1}, {0}, ops)) {
		out.setError("cannot create new dataset");
		GDALClose(hSrcDS);
		return out;
	}

	GDALRasterBandH hSrcBand = GDALGetRasterBand(hSrcDS, 1);
	GDALRasterBandH hTargetBand = GDALGetRasterBand(hDstDS, 1);

	if (GDALComputeProximity(hSrcBand, hTargetBand, papszOptions, NULL, NULL) != CE_None) {
		out.setError("proximity algorithm failed");
		GDALClose(hSrcDS);
		GDALClose(hDstDS);
		CSLDestroy( papszOptions );
		return out;
	}

	GDALClose(hSrcDS);
	CSLDestroy( papszOptions );

	if (driver == "MEM") {
		if (!out.from_gdalMEM(hDstDS, false, true)) {
			out.setError("conversion failed (mem)");
			GDALClose(hDstDS);
			return out;
		}
		GDALClose(hDstDS);
	} else {
		if (!(mask || scale)) {
			double adfMinMax[2];
			GDALComputeRasterMinMax(hTargetBand, true, adfMinMax);
			GDALSetRasterStatistics(hTargetBand, adfMinMax[0], adfMinMax[1], -9999, -9999);
		}
		GDALClose(hDstDS);
		out = SpatRaster(fname, {-1}, {""}, {}, {}, false, false, {});
	}

	if (scale) {
		if (mask) {
			SpatOptions topt(opt);
			out = out.arith(m, "*", false, false, topt);
			out = out.mask(*this, false, mvals, NAN, opt);
		} else {
			out = out.arith(m, "*", false, false, opt);
		}
	} else if (mask) {
		out = out.mask(*this, false, mvals, NAN, opt);
	}
	return out;
}

SpatRaster SpatRaster::sieveFilter(int threshold, int connections, SpatOptions &opt) {

	if (nlyr() > 1) {
		SpatOptions sopt(opt);
		SpatRaster tmp = subset({0}, sopt);
		tmp = tmp.sieveFilter(threshold, connections, opt);
		tmp.addWarning("only the first layer was used");
		return tmp;
	}

	SpatRaster out = geometry(1, true, true, true);

	if (!hasValues()) {
		out.setError("input raster has no values");
		return out;
	}

	if (hasScaleOffset()) {
		SpatOptions opt2(opt);
		SpatRaster app = apply_so(opt2);	
		return app.sieveFilter(threshold, connections, opt);
	}

	if (!((connections == 4) || (connections == 8))) {
		out.setError("connections should be 4 or 8");
		return out;
	}
	if (threshold < 2) {
		out.setError("a threshold < 2 is not meaningful");
		return out;
	}


	if (source[0].extset || source[0].flipped) { 
		SpatOptions topt(opt);
		SpatRaster etmp = hardCopy(topt);
		return etmp.sieveFilter(threshold, connections, opt);
	}


	std::string tmp_filename = "";
	std::string driver = "MEM";
	SpatOptions ops(opt);
	if (!canProcessInMemory(ops)) {
		tmp_filename = tempFile(ops.get_tempdir(), ops.tmpfile, "_sieve.tif");
		driver = "GTiff";
		ops.set_filenames({tmp_filename});
	}

	SpatOptions mopt(opt);
	SpatRaster mask = isnotnan(false, mopt);

	GDALDatasetH hSrcDS, hMskDS, hDstDS;

	if (!open_gdal(hSrcDS, 0, false, ops)) {
		out.setError("cannot open input dataset");
		return out;
	}
	if (!mask.open_gdal(hMskDS, 0, false, ops)) {
		out.setError("cannot open mask dataset");
		return out;
	}

	GDALDriverH hDriver = GDALGetDriverByName( driver.c_str() );
	if ( hDriver == NULL ) {
		out.setError("empty driver");
		return out;
	}
	//opt.datatype = "INT4S";
	if (!out.create_gdalDS(hDstDS, tmp_filename, driver, true, 0, source[0].has_scale_offset, source[0].scale, source[0].offset, ops)) {
		out.setError("cannot create new dataset");
		GDALClose(hSrcDS);
		return out;
	}

	GDALRasterBandH hSrcBand = GDALGetRasterBand(hSrcDS, 1);
	GDALRasterBandH hMskBand = GDALGetRasterBand(hMskDS, 1);
	GDALRasterBandH hTargetBand = GDALGetRasterBand(hDstDS, 1);

	if (GDALSieveFilter(hSrcBand, hMskBand, hTargetBand, threshold, connections, nullptr, NULL, NULL) != CE_None) {
		GDALClose(hSrcDS);
		GDALClose(hMskDS);
		GDALClose(hDstDS);
		out.setError("sieve failed");
		return out;
	}

	GDALClose(hSrcDS);
	GDALClose(hMskDS);

	if (driver == "MEM") {
		if (!out.from_gdalMEM(hDstDS, false, true)) {
			out.setError("conversion failed (mem)");
		}
		GDALClose(hDstDS);

	} else {
		GDALClose(hDstDS);
		out = SpatRaster(tmp_filename, {-1}, {""}, {}, {}, false, false, {});
	}

	opt.names = getNames();
	out.source[0].source_name = "";
	return out.mask(mask, false, 0, NAN, opt);
}



bool getGridderAlgo(std::string algo, GDALGridAlgorithm &a) {
	if (algo == "nearest") {
		a = GGA_NearestNeighbor;
	} else if (algo == "invdistpow") {
		a = GGA_InverseDistanceToAPower;
	} else if (algo == "invdistpownear") {
		a = GGA_InverseDistanceToAPowerNearestNeighbor;
	} else if (algo == "mean") {
		a = GGA_MovingAverage;
	} else if (algo == "min") {
		a = GGA_MetricMinimum;
	} else if (algo == "max") {
		a = GGA_MetricMaximum;
	} else if (algo == "range") {
		a = GGA_MetricRange;
	} else if (algo == "count") {
		a = GGA_MetricCount;
	} else if (algo == "distto") {
		a = GGA_MetricAverageDistance;
	} else if (algo == "distbetween") {
		a = GGA_MetricAverageDistancePts;
	} else if (algo == "linear") {
		a = GGA_Linear;
	} else {
		return false;
	}
	return true;
}


void *metricOptions(std::vector<double> op) {
	GDALGridDataMetricsOptions *poOptions = static_cast<GDALGridDataMetricsOptions *>(
		CPLCalloc(sizeof(GDALGridDataMetricsOptions), 1));

	#if GDAL_VERSION_MAJOR <= 3 && GDAL_VERSION_MINOR < 6
	#else
	poOptions->nSizeOfStructure = sizeof(GDALGridDataMetricsOptions);
	#endif 

	poOptions->dfRadius1 = op[0];
	poOptions->dfRadius2 = op[1];
	poOptions->dfAngle = op[2];
	poOptions->nMinPoints = std::max(0.0, op[3]);
	poOptions->dfNoDataValue = op[4];
	return poOptions;
}

void *invDistPowerOps(std::vector<double> op) {
	GDALGridInverseDistanceToAPowerOptions *poOptions = static_cast<GDALGridInverseDistanceToAPowerOptions *>(
		CPLCalloc(sizeof(GDALGridInverseDistanceToAPowerOptions), 1));

	#if GDAL_VERSION_MAJOR <= 3 && GDAL_VERSION_MINOR < 6
	#else
	poOptions->nSizeOfStructure = sizeof(GDALGridInverseDistanceToAPowerOptions);
	#endif

	poOptions->dfPower = op[0];
	poOptions->dfSmoothing = op[1];
	poOptions->dfRadius1 = op[2];
	poOptions->dfRadius2 = op[3];
	poOptions->dfAngle = op[4];
	poOptions->nMaxPoints = std::max(0.0, op[5]);
	poOptions->nMinPoints = std::max(0.0, op[6]);
	poOptions->dfNoDataValue = op[7];

	poOptions->dfAnisotropyRatio = 1;
	poOptions->dfAnisotropyAngle = 0;

	return poOptions;
}

void *invDistPowerNNOps(std::vector<double> op) {
	GDALGridInverseDistanceToAPowerNearestNeighborOptions *poOptions = static_cast<GDALGridInverseDistanceToAPowerNearestNeighborOptions *>(
		CPLCalloc(sizeof(GDALGridInverseDistanceToAPowerNearestNeighborOptions), 1));

	#if GDAL_VERSION_MAJOR <= 3 && GDAL_VERSION_MINOR < 6
	#else
	poOptions->nSizeOfStructure = sizeof(GDALGridInverseDistanceToAPowerNearestNeighborOptions);
	//poOptions->nMaxPointsPerQuadrant = 
	//poOptions->nMinPointsPerQuadrant = 
	#endif
	poOptions->dfPower = op[0];
	poOptions->dfSmoothing = op[1];
	poOptions->dfRadius = op[2];
	poOptions->nMaxPoints = std::max(0.0, op[3]);
	poOptions->nMinPoints = std::max(0.0, op[4]);
	poOptions->dfNoDataValue = op[5];
	return poOptions;
}

void *moveAvgOps(std::vector<double> op) {
	GDALGridMovingAverageOptions *poOptions = static_cast<GDALGridMovingAverageOptions *>(
		CPLCalloc(sizeof(GDALGridMovingAverageOptions), 1));

	#if GDAL_VERSION_MAJOR <= 3 && GDAL_VERSION_MINOR < 6
	#else
	poOptions->nSizeOfStructure = sizeof(GDALGridMovingAverageOptions);
	#endif


	poOptions->dfRadius1 = op[0];
	poOptions->dfRadius2 = op[1];
	poOptions->dfAngle = op[2];
	poOptions->nMinPoints = std::max(op[3], 0.0);
	poOptions->dfNoDataValue = op[4];
	return poOptions;
}


void *nearngbOps(std::vector<double> op) {
	GDALGridNearestNeighborOptions *poOptions = static_cast<GDALGridNearestNeighborOptions *>(
		CPLCalloc(sizeof(GDALGridNearestNeighborOptions), 1));

	#if GDAL_VERSION_MAJOR <= 3 && GDAL_VERSION_MINOR < 6
	#else
	poOptions->nSizeOfStructure = sizeof(GDALGridNearestNeighborOptions);
	#endif

	poOptions->dfRadius1 = op[0];
	poOptions->dfRadius2 = op[1];
	poOptions->dfAngle = op[2];
	poOptions->dfNoDataValue = op[3];
	return poOptions;
}

void *LinearOps(std::vector<double> op) {
	GDALGridLinearOptions *poOptions = static_cast<GDALGridLinearOptions *>(
		CPLCalloc(sizeof(GDALGridLinearOptions), 1));

	#if GDAL_VERSION_MAJOR <= 3 && GDAL_VERSION_MINOR < 6
	#else
	poOptions->nSizeOfStructure = sizeof(GDALGridLinearOptions);
	#endif

	poOptions->dfRadius = op[0];
	poOptions->dfNoDataValue = op[1];
	return poOptions;
}

SpatRaster SpatRaster::rasterizeWindow(std::vector<double> x, std::vector<double> y, std::vector<double> z, std::string algo, std::vector<double> algops, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	GDALGridAlgorithm eAlg;
		if (!getGridderAlgo(algo, eAlg)) {
		out.setError("unknown algorithm");
		return out;
	}
	void *poOptions;
	if (is_in_vector(algo, {"min", "max", "range", "count", "distto", "distbetween"})) {
		if (algops.size() != 5) {
			out.setError("incorrect algorithm options");
			return out;
		}
		poOptions = metricOptions(algops) ;
	} else if (algo == "mean") {
		if (algops.size() != 5) {
			out.setError("incorrect algorithm options");
			return out;
		}
		poOptions = moveAvgOps(algops);
	} else if (algo == "invdistpow") {
		if (algops.size() != 8) {
			out.setError("incorrect algorithm options");
			return out;
		}
		poOptions = invDistPowerOps(algops);
	} else if (algo == "invdistpownear") {
		if (algops.size() != 6) {
			out.setError("incorrect algorithm options");
			return out;
		}
		poOptions = invDistPowerNNOps(algops);
	} else if (algo == "nearest") {
		if (algops.size() != 4) {
			out.setError("incorrect algorithm options");
			return out;
		}
		poOptions = nearngbOps(algops);
	} else if (algo == "linear") {
		if (algops.size() != 2) {
			out.setError("incorrect algorithm options");
			return out;
		}
		poOptions = LinearOps(algops);
	} else {
		out.setError("unknown algorithm");
		return out;
	}

	SpatExtent e = out.getExtent();
	if (!out.writeStart(opt, out.filenames())) {
		return out;
	}

	const char *old_count_value = CPLGetConfigOption("GDAL_GRID_POINT_COUNT_THRESHOLD", NULL);
	std::string n = std::to_string(x.size());
	CPLSetConfigOption("GDAL_GRID_POINT_COUNT_THRESHOLD", n.c_str());

	GUInt32 npts = x.size();
	GDALGridContext *ctxt = GDALGridContextCreate(eAlg, poOptions, npts, &x[0], &y[0], &z[0], true);
	CPLFree( poOptions );

	double rsy = out.yres() / 2;
	size_t ncs = out.ncol();
	BlockSize bs = out.getBlockSize(opt);
	std::vector<double> v;
	for (size_t i=0; i < bs.n; i++) {
		double ymax = yFromRow(bs.row[i]) + rsy;
		double ymin = yFromRow(bs.row[i] + bs.nrows[i] - 1) - rsy;
		v.resize(bs.nrows[i] * ncs);

		CPLErr eErr = GDALGridContextProcess(ctxt, e.xmin, e.xmax, ymin, ymax, ncs, bs.nrows[i], GDT_Float64, &v[0], NULL, NULL);

		if ( eErr != CE_None ) {
			out.setError("something went wrong");
			GDALGridContextFree(ctxt);
			CPLSetConfigOption("GDAL_GRID_POINT_COUNT_THRESHOLD", old_count_value);
			return out;
		}

		std::vector<double> f;
		f.reserve(v.size());
		for (size_t j=0; j < bs.nrows[i]; j++) {
			unsigned start = (bs.nrows[i] - 1 - j) * ncs;
			f.insert(f.end(), v.begin()+start, v.begin()+start+ncs);
		}
		if (!out.writeBlock(f, i)) {
			GDALGridContextFree(ctxt);
			CPLSetConfigOption("GDAL_GRID_POINT_COUNT_THRESHOLD", old_count_value);
			return out;
		}
	}
	GDALGridContextFree(ctxt);
	CPLSetConfigOption("GDAL_GRID_POINT_COUNT_THRESHOLD", old_count_value);

	out.writeStop();
	return out;
}



/*
SpatRaster SpatRaster::fillna(int threshold, int connections, SpatOptions &opt) {
CPLErr GDALFillNodata(GDALRasterBandH hTargetBand, GDALRasterBandH hMaskBand, doubledfMaxSearchDist, intbDeprecatedOption, intnSmoothingIterations, char**papszOptions, GDALProgressFuncpfnProgress, void*pProgressArg)
*/


SpatRaster SpatRaster::fillNA(double missing, double maxdist, int niter, SpatOptions &opt) {

	SpatRaster out = geometry(1, true, true, true);

	if (!hasValues()) {
		out.setError("input raster has no values");
		return out;
	}

	if (hasScaleOffset()) {
		SpatOptions opt2(opt);
		SpatRaster app = apply_so(opt2);	
		return app.fillNA(missing, maxdist, niter, opt);
	}


	if (maxdist <= 0) {
		out.setError("maxdist should be > 0");
		return out;
	}
	if (niter < 0) {
		out.setError("niter should be >= 0");
		return out;
	}

	std::string filename = opt.get_filename();
	std::string driver;
	if (filename.empty()) {
		if (canProcessInMemory(opt)) {
			driver = "MEM";
		} else {
			filename = tempFile(opt.get_tempdir(), opt.tmpfile, ".tif");
			opt.set_filenames({filename});
			driver = "GTiff";
		}
	} else {
		driver = opt.get_filetype();
		getGDALdriver(filename, driver);
		if (driver.empty()) {
			setError("cannot guess file type from filename");
			return out;
		}
		std::string errmsg;
		if (!can_write({filename}, filenames(), opt.get_overwrite(), errmsg)) {
			out.setError(errmsg);
			return out;
		}
	}

	SpatOptions ops(opt);
	GDALDatasetH hSrcDS, hDstDS;
	if (!open_gdal(hSrcDS, 0, false, ops)) {
		out.setError("cannot open input dataset");
		return out;
	}

	GDALDriverH hDriver = GDALGetDriverByName( driver.c_str() );
	if ( hDriver == NULL ) {
		out.setError("empty driver");
		return out;
	}

	//opt.datatype = "INT4S";
	if (!out.create_gdalDS(hDstDS, filename, driver, true, 0, source[0].has_scale_offset, source[0].scale, source[0].offset, opt)) {
		out.setError("cannot create new dataset");
		GDALClose(hSrcDS);
		return out;
	}

	GDALRasterBandH hSrcBand = GDALGetRasterBand(hSrcDS, 1);
	GDALRasterBandH hTargetBand = GDALGetRasterBand(hDstDS, 1);

	if (GDALFillNodata(hTargetBand, hSrcBand, maxdist, 0, niter, NULL, NULL, NULL) != CE_None) {
		out.setError("fillNA failed");
		GDALClose(hSrcDS);
		GDALClose(hDstDS);
		return out;
	}

	GDALClose(hSrcDS);
	if (driver == "MEM") {
		if (!out.from_gdalMEM(hDstDS, false, true)) {
			out.setError("conversion failed (mem)");
		}
		GDALClose(hDstDS);
		return out;
	}

	double adfMinMax[2];
	GDALComputeRasterMinMax(hTargetBand, true, adfMinMax);
	GDALSetRasterStatistics(hTargetBand, adfMinMax[0], adfMinMax[1], -9999, -9999);
	GDALClose(hDstDS);
	return SpatRaster(filename, {-1}, {""}, {}, {}, false, false, {});
}




/*
#include <gdalpansharpen.h>
SpatRaster SpatRaster::panSharpen(SpatRaster pan, SpatOptions &opt) {
	SpatRaster out = geometry();
	return out;
}
*/



