// 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 "spatRaster.h"
#include "distance.h"
#include <limits>
#include <cmath>
#include <algorithm>
#include <map>
#include <functional>
#include "geodesic.h"
#include "recycle.h"
#include "math_utils.h"
#include "vecmath.h"
#include "file_utils.h"
#include "string_utils.h"
#include "crs.h"
#include "sort.h"
#include "geosphere.h"
#include "tbb_helper.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#ifndef M_2PI
#define M_2PI (2.0 * M_PI)
#endif

/*
inline void shortDistPoints(std::vector<double> &d, const std::vector<double> &x, const std::vector<double> &y, const std::vector<double> &px, const std::vector<double> &py, const bool& lonlat, const std::string& method, const double &lindist) {
	if (lonlat) {
		distanceToNearest_lonlat(d, x, y, px, py, lindist, method);
	} else {
		distanceToNearest_plane(d, x, y, px, py, lindist);
	}
}
*/

inline void shortDirectPoints(std::vector<double> &d, std::vector<double> &x, std::vector<double> &y, std::vector<double> &px, std::vector<double> &py, const bool& lonlat, bool &from, bool &degrees, const std::string &method) {
	if (lonlat) {
		directionToNearest_lonlat(d, x, y, px, py, degrees, from, method);
	} else {
		directionToNearest_plane(d, x, y, px, py, degrees, from);
	}
}


/*
bool get_m(double &m, SpatSRS srs, bool lonlat, std::string unit) {
	m = 1;
	if (!lonlat) {
		m = srs.to_meter();
		m = std::isnan(m) ? 1 : m;
	}
	std::vector<std::string> ss {"m", "km"};
	if (std::find(ss.begin(), ss.end(), unit) == ss.end()) {
		return false;
	}
	if (unit == "km")	{
		m /= 1000;
	}
	return true;
}
*/

std::vector<double> dist_bounds(const std::vector<double>& vx, const std::vector<double>& vy, const std::vector<double>& rx, const double& ry, size_t& first, size_t& last, const bool& lonlat, const std::string &method) {

	std::vector<double> d(rx.size(), std::numeric_limits<double>::max());
	size_t oldfirst = first;
	first = vx.size();
	last = 0;


	if (lonlat) {
		std::function<double(double, double, double, double)> dfun;
		if (method == "haversine") {
			dfun = distance_hav;
		} else if (method == "cosine") {
			dfun = distance_cos;			
		} else {
			dfun = distance_geo;
		}


		for (size_t i=0; i<rx.size(); i++) {
			size_t thisone = 0;
			for (size_t j=oldfirst; j<vx.size(); j++) {
				double dd = dfun(rx[i], ry, vx[j], vy[j]);
				if (dd < d[i]) {
					d[i] = dd;
					thisone = j;
				}
			}
			first = std::min(thisone, first);
			last  = std::max(thisone, last);
		}
	} else {
		for (size_t i=0; i<rx.size(); i++) {
			size_t thisone = 0;
			for (size_t j=oldfirst; j<vx.size(); j++) {
				double dd = distance_plane(rx[i], ry, vx[j], vy[j]);
				if (dd < d[i]) {
					d[i] = dd;
					thisone = j;
				}
			}
			first = std::min(thisone, first);
			last  = std::max(thisone, last);
		}
	}
	last += 1;
	return d;
}

void dist_only(std::vector<double> &d, const std::vector<double>& vx, const std::vector<double>& vy, const std::vector<double>& rx, const std::vector<double>& ry, const size_t& first, const size_t& last, const bool& lonlat, const std::vector<double>& dlast, bool skip, const std::vector<double>& v, const std::string& method, bool setNA) {

	size_t rxs = rx.size();
	d.reserve(rxs + dlast.size());

	double inf = std::numeric_limits<double>::infinity();

	if (lonlat) {

		if (method == "geo") {

			double dd, azi1, azi2;
			struct geod_geodesic g;
			// get a and f from crs?
			double a = 6378137.0;
			double f = 1/298.257223563;
			geod_init(&g, a, f);

			if (skip) {
				for (size_t i=0; i<rxs; i++) {
					if (std::isnan(v[i])) {
						d.push_back(inf);
						for (size_t j=first; j<last; j++) {
							geod_inverse(&g, ry[i], rx[i], vy[j], vx[j], &dd, &azi1, &azi2);
							if (dd < d[i]) {
								d[i] = dd;
							}
						}
					} else {
						d.push_back(0);
					}
				}
			} else { // no skip

				for (size_t i=0; i<rxs; i++) {
					d.push_back(inf);
					for (size_t j=first; j<last; j++) {
						geod_inverse(&g, ry[i], rx[i], vy[j], vx[j], &dd, &azi1, &azi2);
						if (dd < d[i]) {
							d[i] = dd;
						}
					}
				} 
			}

		} else { // not geo

			std::function<double(double, double, double, double)> dfun;
			if (method == "haversine") {
				dfun = distance_hav;
			} else if (method == "cosine") {
				dfun = distance_cos;
			} 
			if (skip) {

				for (size_t i=0; i<rxs; i++) {
					if (std::isnan(v[i])) {
						d.push_back(inf);
						for (size_t j=first; j<last; j++) {
							double dd = dfun(rx[i], ry[i], vx[j], vy[j]);
							if (dd < d[i]) {
								d[i] = dd;
							}
						}
					} else {
						d.push_back(0);
					}
				}		
			} else {	
				for (size_t i=0; i<rxs; i++) {
					d.push_back(inf);
					for (size_t j=first; j<last; j++) {
						//double dd = distHaversine(rx[i], ry[i], vx[j], vy[j]);
						double dd = dfun(rx[i], ry[i], vx[j], vy[j]);

						if (dd < d[i]) {
							d[i] = dd;
						}
					}
				}
			} 
		}

	} else { // not lonlat
		if (skip) {
			for (size_t i=0; i<rxs; i++) {
				if (std::isnan(v[i])) {
					d.push_back(inf);
					for (size_t j=first; j<last; j++) {
						double dd = distance_plane(rx[i], ry[i], vx[j], vy[j]);
						if (dd < d[i]) {
							d[i] = dd;
						}
					}
				} else {
					d.push_back(0);
				}
			}
		} else {
			for (size_t i=0; i<rxs; i++) {
				d.push_back(inf);
				for (size_t j=first; j<last; j++) {
					double dd = distance_plane(rx[i], ry[i], vx[j], vy[j]);
					if (dd < d[i]) {
						d[i] = dd;
					}
				}
			}
		}
	}

	d.insert(d.end(), dlast.begin(), dlast.end());
	if (skip) {
		for (size_t i=rxs; i< v.size(); i++) {
			if (!std::isnan(v[i])) {
				d[i] = 0;
			}
		}
		if (setNA) {
			double mxval = std::numeric_limits<double>::max();
			for (size_t i=0; i< v.size(); i++) {
				if (v[i] == mxval) {
					d[i] = NAN;
				}
			}
		}
	}
}


SpatRaster SpatRaster::distance_crds(std::vector<double>& x, std::vector<double>& y, const std::string& method, bool skip, bool setNA, std::string unit, double max_dist, SpatOptions &opt) {

	SpatRaster out = geometry();
	if (x.empty()) {
		out.setError("no locations to compute distance from");
		return(out);
	}
	const double toRad = 0.0174532925199433;
	std::vector<std::size_t> pm = sort_order_d(y);
	permute(x, pm);
	permute(y, pm);

	if (source[0].srs.is_empty()) {
		out.addWarning("unknown CRS. Results can be wrong");
	}
	bool lonlat = is_lonlat(); 

	double m=1;
	if (!source[0].srs.m_dist(m, lonlat, unit)) {
		out.setError("invalid unit");
		return(out);
	}

	size_t nc = ncol();
	if (nrow() > 1000) {
		opt.steps = std::max(opt.steps, (size_t) 4);
		opt.progress = opt.progress * 2;
	}

 	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}
	std::vector<double> cells;
	std::vector<double> dlast;

	std::vector<int64_t> cols;
	cols.resize(ncol());
	std::iota(cols.begin(), cols.end(), 0);
	std::vector<double> tox = xFromCol(cols);

	if (lonlat && (method != "geo")) {
		for (double &d : x) d *= toRad;
		for (double &d : y) d *= toRad;
		for (double &d : tox) d *= toRad;
	}

	size_t oldfirst = 0;
	size_t first = 0;
	size_t last  = x.size();

	std::vector<double> v;
	if (skip) {
		if (!readStart()) {
			out.setError(getError());
			return(out);
		}
		for (size_t i = 0; i < out.bs.n; i++) {
			cells.resize((out.bs.nrows[i] -1) * nc) ;
			std::iota(cells.begin(), cells.end(), out.bs.row[i] * nc);
			std::vector<std::vector<double>> rxy = xyFromCell(cells);
			double toy = yFromRow(out.bs.row[i] + out.bs.nrows[i] - 1);
			if (lonlat && (method != "geo")) {
				toy *= toRad;
				for (double &d : rxy[0]) d *= toRad;
				for (double &d : rxy[1]) d *= toRad;
			}
			readBlock(v, out.bs, i);
			dlast = dist_bounds(x, y, tox, toy, first, last, lonlat, method);
			std::vector<double> d;
			dist_only(d, x, y, rxy[0], rxy[1], oldfirst, last, lonlat, dlast, true, v, method, setNA);
			oldfirst = first;
			if (m != 1) {
				for (double &v : d) v *= m;
			}
			if (max_dist > 0) {
				for (double &v : d) v = v > max_dist ? NAN : v;
			}
			if (!out.writeBlock(d, i)) return out;
		}
		readStop();
	} else {
		for (size_t i = 0; i < out.bs.n; i++) {
			double toy = yFromRow(out.bs.row[i] + out.bs.nrows[i] - 1);
			cells.resize((out.bs.nrows[i] -1) * nc) ;
			std::iota(cells.begin(), cells.end(), out.bs.row[i] * nc);
			std::vector<std::vector<double>> rxy = xyFromCell(cells);
			if (lonlat && (method != "geo")) {
				toy *= toRad;
				for (double &d : rxy[0]) d *= toRad;
				for (double &d : rxy[1]) d *= toRad;
			}
			dlast = dist_bounds(x, y, tox, toy, first, last, lonlat, method);
			std::vector<double> d;
			dist_only(d, x, y, rxy[0], rxy[1], oldfirst, last, lonlat, dlast, false, v, method, setNA);
			oldfirst = first;
			if (m != 1) {
				for (double &v : d) v *= m;
			}
			if (max_dist > 0) {
				for (double &v : d) v = v > max_dist ? NAN : v;
			}
			if (!out.writeBlock(d, i)) return out;
		}
	}
	out.writeStop();
	return(out);
}


// ----------------------------------------------------------------------------
// Fast path for distance(SpatRaster, target) for lon/lat with the
// haversine/cosine method. Computes, for every cell in the output, the
// distance to the nearest point in the supplied (px, py) coordinate set
// (the edge cells around target/exclude). It is a drop-in equivalent of
// distance_crds(skip=true) but much faster because:
//   1. Edge points are sorted by latitude once. Their sin/cos are cached.
//   2. For each output row we cache cos(py)/sin(py).
//   3. Search starts from the latitude-nearest point (binary search) and
//      walks outward in both directions. The cheap latitude-only lower
//      bound (geodesic angular distance >= |dlat|) is exact: comparing it
//      to the current best in radians lets us terminate the walk in each
//      direction the moment the bound exceeds the best, without trig.
//   4. When max_dist > 0 we additionally seed the per-cell best distance
//      with that threshold (converted to radians), so cells whose nearest
//      edge point is beyond max_dist terminate the walk in both directions
//      almost immediately and never pay for trig. The NaN is produced by
//      the existing post-filter.
//   5. TBB parallelism over rows within a block.
//
// set TERRA_DIST_SLOW=1 to bypass the fast track 
// ----------------------------------------------------------------------------

SpatRaster SpatRaster::distance_crds_lonlat_fast(std::vector<double>& x, std::vector<double>& y, const std::string& method, bool skip, bool setNA, std::string unit, double max_dist, SpatOptions &opt) {

	SpatRaster out = geometry();
	if (x.empty()) {
		out.setError("no locations to compute distance from");
		return out;
	}
	if ((method != "haversine") && (method != "cosine")) {
		out.setError("internal: distance_crds_lonlat_fast called with unsupported method");
		return out;
	}
	bool lonlat = is_lonlat();
	if (!lonlat) {
		out.setError("internal: distance_crds_lonlat_fast called with non-lonlat raster");
		return out;
	}

	if (source[0].srs.is_empty()) {
		out.addWarning("unknown CRS. Results can be wrong");
	}

	double m = 1;
	if (!source[0].srs.m_dist(m, lonlat, unit)) {
		out.setError("invalid unit");
		return out;
	}

	const double D2R = 0.017453292519943295;
	const double EARTH_R = 6378137.0;
	const double inf = std::numeric_limits<double>::infinity();
	const double mxval = std::numeric_limits<double>::max();
	const bool use_cos = (method == "cosine");

	// When max_dist > 0 the result distances are NaN'd for values strictly
	// greater than max_dist. Converting that threshold into an angular bound
	// lets the latitude-only prune (|dlat| >= cur_ang) kill any edge point
	// that is outside the "relevant" ring around the cell without spending
	// any trig. seed_ang is nudged one ulp above max_ang so that genuine
	// points at d == max_dist still update cur_ang (preserving the original
	// ">" semantics) while the seed itself gets NaN'd by the post-filter.
	const double max_ang = (max_dist > 0) ? (max_dist / EARTH_R / m) : inf;
	const double seed_ang = std::isfinite(max_ang) ? std::nextafter(max_ang, inf) : inf;

	// Sort edge points by latitude (ascending) so we can binary-search for the
	// row latitude and walk outward, pruning by |dlat|.
	std::vector<std::size_t> pm = sort_order_a(y);
	permute(x, pm);
	permute(y, pm);

	const size_t np = x.size();
	std::vector<double> qx_rad(np), qy_rad(np), sin_qy(np), cos_qy(np);
	for (size_t j = 0; j < np; j++) {
		qx_rad[j] = x[j] * D2R;
		qy_rad[j] = y[j] * D2R;
		sin_qy[j] = sin(qy_rad[j]);
		cos_qy[j] = cos(qy_rad[j]);
	}

	const size_t nc = ncol();
	std::vector<int64_t> cols(nc);
	std::iota(cols.begin(), cols.end(), 0);
	std::vector<double> tox = xFromCol(cols);
	std::vector<double> tox_rad(nc);
	for (size_t c = 0; c < nc; c++) tox_rad[c] = tox[c] * D2R;

	if (nrow() > 1000) {
		opt.steps = std::max(opt.steps, (size_t) 4);
		opt.progress = opt.progress * 2;
	}

	if (skip) {
		if (!readStart()) {
			out.setError(getError());
			return out;
		}
	}
	if (!out.writeStart(opt, filenames())) {
		if (skip) readStop();
		return out;
	}

	for (size_t b = 0; b < out.bs.n; b++) {
		const size_t nrow_blk = out.bs.nrows[b];
		const size_t nblk = nrow_blk * nc;

		std::vector<double> v;
		if (skip) {
			readBlock(v, out.bs, b);
		}

		std::vector<double> py_rad(nrow_blk), sin_py(nrow_blk), cos_py(nrow_blk);
		for (size_t r = 0; r < nrow_blk; r++) {
			py_rad[r] = yFromRow(out.bs.row[b] + r) * D2R;
			sin_py[r] = sin(py_rad[r]);
			cos_py[r] = cos(py_rad[r]);
		}

		std::vector<double> d(nblk);

		auto process_row = [&](size_t r) {
			const double py = py_rad[r];
			const double spy = sin_py[r];
			const double cpy = cos_py[r];
			const size_t row_off = r * nc;

			// First point with qy >= py; we walk outward from here.
			const size_t mid = std::lower_bound(qy_rad.begin(), qy_rad.end(), py) - qy_rad.begin();

			for (size_t c = 0; c < nc; c++) {
				const size_t idx = row_off + c;
				if (skip && !std::isnan(v[idx])) {
					d[idx] = 0.0;
					continue;
				}
				const double px = tox_rad[c];
				double cur_ang = seed_ang;  // angular distance, radians

				// Forward direction: qy >= py, dlat increases monotonically.
				for (size_t j = mid; j < np; j++) {
					const double dlat = qy_rad[j] - py;
					if (dlat >= cur_ang) break;  // |dlat| is a lower bound on geodesic angle
					double dd;
					if (use_cos) {
						double cosD = spy * sin_qy[j] + cpy * cos_qy[j] * cos(px - qx_rad[j]);
						if (cosD > 1.0) cosD = 1.0;
						else if (cosD < -1.0) cosD = -1.0;
						dd = acos(cosD);
					} else {
						const double sLat = sin(dlat * 0.5);
						const double sLon = sin((qx_rad[j] - px) * 0.5);
						double a = sLat * sLat + cpy * cos_qy[j] * sLon * sLon;
						if (a < 0.0) a = 0.0;
						else if (a > 1.0) a = 1.0;
						dd = 2.0 * atan2(sqrt(a), sqrt(1.0 - a));
					}
					if (dd < cur_ang) cur_ang = dd;
				}
				// Backward direction: qy < py, dlat increases monotonically.
				for (size_t jj = mid; jj > 0; jj--) {
					const size_t j = jj - 1;
					const double dlat = py - qy_rad[j];
					if (dlat >= cur_ang) break;
					double dd;
					if (use_cos) {
						double cosD = spy * sin_qy[j] + cpy * cos_qy[j] * cos(px - qx_rad[j]);
						if (cosD > 1.0) cosD = 1.0;
						else if (cosD < -1.0) cosD = -1.0;
						dd = acos(cosD);
					} else {
						const double sLat = sin(dlat * 0.5);
						const double sLon = sin((qx_rad[j] - px) * 0.5);
						double a = sLat * sLat + cpy * cos_qy[j] * sLon * sLon;
						if (a < 0.0) a = 0.0;
						else if (a > 1.0) a = 1.0;
						dd = 2.0 * atan2(sqrt(a), sqrt(1.0 - a));
					}
					if (dd < cur_ang) cur_ang = dd;
				}

				d[idx] = cur_ang * EARTH_R;
			}
		};

#if defined(USE_TBB)
		if (opt.parallel) {
			terra_parallel_for(opt, tbb::blocked_range<size_t>(0, nrow_blk),
				[&](const tbb::blocked_range<size_t>& range) {
					for (size_t r = range.begin(); r != range.end(); r++) {
						process_row(r);
					}
				});
		} else {
			for (size_t r = 0; r < nrow_blk; r++) process_row(r);
		}
#else
		for (size_t r = 0; r < nrow_blk; r++) process_row(r);
#endif

		if (m != 1.0) {
			for (double &val : d) val *= m;
		}
		if (max_dist > 0) {
			for (double &val : d) val = val > max_dist ? NAN : val;
		}
		if (skip && setNA) {
			for (size_t i = 0; i < nblk; i++) {
				if (v[i] == mxval) d[i] = NAN;
			}
		}

		if (!out.writeBlock(d, b)) {
			if (skip) readStop();
			return out;
		}
	}

	if (skip) readStop();
	out.writeStop();
	return out;
}


// ----------------------------------------------------------------------------
// Fast path for distance(SpatRaster, target) for lon/lat with the 
// very precise method by Karney
//
// Same structure as the haversine/cosine fast path, with these differences:
//   * distances are in meters throughout (no radian<->meters round trip)
//   * the per-cell best is seeded from max_dist directly
//   * the latitude-only lower bound uses R_min = a * (1 - e^2), the minimum
//     meridional radius of curvature, which is a true lower bound on the
//     geodesic distance given |dlat_rad|. 
//
// ----------------------------------------------------------------------------

SpatRaster SpatRaster::distance_crds_lonlat_fast_geo(std::vector<double>& x, std::vector<double>& y, bool skip, bool setNA, std::string unit, double max_dist, SpatOptions &opt) {

	SpatRaster out = geometry();
	if (x.empty()) {
		out.setError("no locations to compute distance from");
		return out;
	}
	bool lonlat = is_lonlat();
	if (!lonlat) {
		out.setError("internal: distance_crds_lonlat_fast_geo called with non-lonlat raster");
		return out;
	}

	if (source[0].srs.is_empty()) {
		out.addWarning("unknown CRS. Results can be wrong");
	}

	double m = 1;
	if (!source[0].srs.m_dist(m, lonlat, unit)) {
		out.setError("invalid unit");
		return out;
	}

	const double D2R = 0.017453292519943295;
	const double inf = std::numeric_limits<double>::infinity();
	const double mxval = std::numeric_limits<double>::max();

	// Ellipsoid parameters from the CRS (WGS84 fallback). R_min is the smallest
	// possible meridional radius of curvature, used as a lower bound on the
	// geodesic distance given a latitude difference.
	double geo_a, geo_f;
	geo_ellipsoid_from_wkt(source[0].srs.wkt, geo_a, geo_f);
	const double e2 = geo_f * (2.0 - geo_f);
	const double R_min = geo_a * (1.0 - e2);
	struct geod_geodesic g;
	geod_init(&g, geo_a, geo_f);

	// Sort edge points by latitude (ascending).
	std::vector<std::size_t> pm = sort_order_a(y);
	permute(x, pm);
	permute(y, pm);

	const size_t np = x.size();
	// Keep coordinates in degrees (what geod_inverse needs) and cache the
	// latitude in radians for the cheap |dlat| prune.
	std::vector<double> qy_rad(np);
	for (size_t j = 0; j < np; j++) qy_rad[j] = y[j] * D2R;

	const size_t nc = ncol();
	std::vector<int64_t> cols(nc);
	std::iota(cols.begin(), cols.end(), 0);
	std::vector<double> tox = xFromCol(cols);   // degrees

	if (nrow() > 1000) {
		opt.steps = std::max(opt.steps, (size_t) 4);
		opt.progress = opt.progress * 2;
	}

	if (skip) {
		if (!readStart()) {
			out.setError(getError());
			return out;
		}
	}
	if (!out.writeStart(opt, filenames())) {
		if (skip) readStop();
		return out;
	}

	// maxdist is specified in output units (m if unit="m", km if unit="km").
	// Internally we work in meters, so convert once. seed_dist sits one ulp
	// above the threshold so a real distance d == max_dist still updates
	// cur_dist (preserving the original "strictly greater than" NaN rule),
	// while a cell that never finds a point within max_dist stays at seed_dist
	// and gets NaN'd by the post-filter.
	const double max_m = (max_dist > 0) ? (max_dist / m) : inf;
	const double seed_dist = std::isfinite(max_m) ? std::nextafter(max_m, inf) : inf;

	for (size_t b = 0; b < out.bs.n; b++) {
		const size_t nrow_blk = out.bs.nrows[b];
		const size_t nblk = nrow_blk * nc;

		std::vector<double> v;
		if (skip) {
			readBlock(v, out.bs, b);
		}

		std::vector<double> py_deg(nrow_blk), py_rad(nrow_blk);
		for (size_t r = 0; r < nrow_blk; r++) {
			py_deg[r] = yFromRow(out.bs.row[b] + r);
			py_rad[r] = py_deg[r] * D2R;
		}

		std::vector<double> d(nblk);

		auto process_row = [&](size_t r) {
			const double py_d = py_deg[r];
			const double py_r = py_rad[r];
			const size_t row_off = r * nc;
			const size_t mid = std::lower_bound(qy_rad.begin(), qy_rad.end(), py_r) - qy_rad.begin();

			for (size_t c = 0; c < nc; c++) {
				const size_t idx = row_off + c;
				if (skip && !std::isnan(v[idx])) {
					d[idx] = 0.0;
					continue;
				}
				const double px_d = tox[c];
				double cur_dist = seed_dist;  // meters

				// Forward: qy >= py. |dlat| grows monotonically.
				for (size_t j = mid; j < np; j++) {
					const double dlat_m_lb = R_min * (qy_rad[j] - py_r);
					if (dlat_m_lb >= cur_dist) break;
					double dd, azi1, azi2;
					geod_inverse(&g, py_d, px_d, y[j], x[j], &dd, &azi1, &azi2);
					if (dd < cur_dist) cur_dist = dd;
				}
				// Backward: qy < py.
				for (size_t jj = mid; jj > 0; jj--) {
					const size_t j = jj - 1;
					const double dlat_m_lb = R_min * (py_r - qy_rad[j]);
					if (dlat_m_lb >= cur_dist) break;
					double dd, azi1, azi2;
					geod_inverse(&g, py_d, px_d, y[j], x[j], &dd, &azi1, &azi2);
					if (dd < cur_dist) cur_dist = dd;
				}

				d[idx] = cur_dist;
			}
		};

#if defined(USE_TBB)
		if (opt.parallel) {
			terra_parallel_for(opt, tbb::blocked_range<size_t>(0, nrow_blk),
				[&](const tbb::blocked_range<size_t>& range) {
					for (size_t r = range.begin(); r != range.end(); r++) {
						process_row(r);
					}
				});
		} else {
			for (size_t r = 0; r < nrow_blk; r++) process_row(r);
		}
#else
		for (size_t r = 0; r < nrow_blk; r++) process_row(r);
#endif

		if (m != 1.0) {
			for (double &val : d) val *= m;
		}
		if (max_dist > 0) {
			for (double &val : d) val = val > max_dist ? NAN : val;
		}
		if (skip && setNA) {
			for (size_t i = 0; i < nblk; i++) {
				if (v[i] == mxval) d[i] = NAN;
			}
		}

		if (!out.writeBlock(d, b)) {
			if (skip) readStop();
			return out;
		}
	}

	if (skip) readStop();
	out.writeStop();
	return out;
}


// ----------------------------------------------------------------------------
// Fast path for distance(SpatRaster, SpatVector) for lon/lat polygons or lines
// using the haversine/cosine method.
//
// Speedups over the brute-force path come from:
//   1. Pre-extracting all segments once into a flat array (avoids per-block
//      SpatVector reconstruction and repeated dispatch).
//   2. Computing per-segment lat/lon bounding boxes that include the
//      great-circle bulge (sampled along the geodesic). This makes a cheap
//      lower-bound test correct even for very long edges (e.g. a polygon
//      edge spanning >100 deg of longitude).
//   3. Two-stage pruning per cell: first a near-free latitude-only lower
//      bound (cells in a row share latitude, so this is computed once per
//      row/segment), then a full bbox lower bound. The expensive
//      dist2segment_cos call is only made when both lower bounds are
//      smaller than the current best distance for the cell.
//   4. A single pointInPolygon call per block instead of per-cell relate dispatch.
//   5. TBB parallelism over rows within a block.
// ----------------------------------------------------------------------------

namespace {

struct LLSeg {
	double x1, y1, x2, y2;            // endpoint coords, radians
	double minx, maxx, miny, maxy;    // tight bbox (incl. bulge), radians
	bool wraps;                       // bbox crosses the antimeridian
};

// Compute a tight bbox of the great-circle arc between two lon/lat points
// (in degrees). The bbox is returned in radians. We sample a handful of
// points along the geodesic so the bbox contains the latitudinal
// bulge that long arcs make toward the nearer pole.
static void compute_seg_bbox(double lon1d, double lat1d,
                             double lon2d, double lat2d, LLSeg &s) {
	static const double D2R = 0.017453292519943295;
	s.x1 = lon1d * D2R;  s.y1 = lat1d * D2R;
	s.x2 = lon2d * D2R;  s.y2 = lat2d * D2R;

	// Make lon2 continuous with lon1 (resolves antimeridian crossing).
	double dlon = lon2d - lon1d;
	while (dlon >  180.0) dlon -= 360.0;
	while (dlon < -180.0) dlon += 360.0;
	double lon2_cont = lon1d + dlon;

	// Sample on a unit sphere; geod_inverse returns d in radians for a=1, f=0.
	geod_geodesic g;
	geod_init(&g, 1.0, 0.0);
	double d, azi1, azi2;
	geod_inverse(&g, lat1d, lon1d, lat2d, lon2d, &d, &azi1, &azi2);

	const int N = 12;
	double minx_d = std::min(lon1d, lon2_cont);
	double maxx_d = std::max(lon1d, lon2_cont);
	double miny_d = std::min(lat1d, lat2d);
	double maxy_d = std::max(lat1d, lat2d);

	if (d > 0.0) {
		for (int i = 1; i < N; i++) {
			double frac = static_cast<double>(i) / N;
			double lat_o, lon_o, azi_o;
			geod_direct(&g, lat1d, lon1d, azi1, d * frac, &lat_o, &lon_o, &azi_o);
			while (lon_o - lon1d >  180.0) lon_o -= 360.0;
			while (lon_o - lon1d < -180.0) lon_o += 360.0;
			if (lon_o < minx_d) minx_d = lon_o;
			if (lon_o > maxx_d) maxx_d = lon_o;
			if (lat_o < miny_d) miny_d = lat_o;
			if (lat_o > maxy_d) maxy_d = lat_o;
		}
	}

	s.miny = miny_d * D2R;
	s.maxy = maxy_d * D2R;

	// Map bbox back to [-180, 180]. If after mapping minx > maxx, the
	// bbox spans the antimeridian and we mark it as wrapping.
	double mn = minx_d, mx = maxx_d;
	while (mn < -180.0) mn += 360.0;
	while (mn >  180.0) mn -= 360.0;
	while (mx < -180.0) mx += 360.0;
	while (mx >  180.0) mx -= 360.0;
	if (maxx_d - minx_d >= 360.0) {
		s.wraps = false;
		s.minx = -M_PI;
		s.maxx =  M_PI;
	} else {
		s.wraps = (mn > mx);
		s.minx = mn * D2R;
		s.maxx = mx * D2R;
	}
}

// Haversine angular distance (radians) used inside lower-bound helpers.
inline double hav_rad(double px, double py, double qx, double qy,
                     double cos_py) {
	double dLat = qy - py;
	double dLon = qx - px;
	if (dLon >  M_PI) dLon -= M_2PI;
	if (dLon < -M_PI) dLon += M_2PI;
	double sLat = sin(dLat * 0.5);
	double sLon = sin(dLon * 0.5);
	double a = sLat * sLat + cos_py * cos(qy) * sLon * sLon;
	if (a < 0.0) a = 0.0;
	if (a > 1.0) a = 1.0;
	return 2.0 * asin(sqrt(a));
}

// True lower bound on the geodesic distance from point (px, py) (in radians)
// to a segment's bounding box, in meters. Computes the *closest point on the
// bbox* in geodesic distance properly: for the top/bottom edges (constant
// latitude), the longitude that minimises distance is the cell's longitude
// clamped to the bbox lon range. For the left/right edges (constant
// longitude), the optimal latitude is found analytically from
// d/dqy [sin(py)*sin(qy) + cos(py)*cos(dlon)*cos(qy)] = 0
// which gives qy* = atan2(sin(py), cos(py)*cos(dlon)) when that lies in
// (-pi/2, pi/2); otherwise the optimum on [miny, maxy] is at one of the
// endpoints. We evaluate both endpoints unconditionally and the analytic
// optimum when it falls inside the bbox.
//
inline double bbox_lower_bound_m(double px, double py, double cos_py, const LLSeg &s) {
	static const double EARTH_R = 6378137.0;

	bool inside_lat = (py >= s.miny && py <= s.maxy);
	bool inside_lon;
	if (s.wraps) {
		inside_lon = (px >= s.minx) || (px <= s.maxx);
	} else {
		inside_lon = (px >= s.minx && px <= s.maxx);
	}
	if (inside_lat && inside_lon) return 0.0;

	// Closest qx in bbox lon range to px.
	double qx_clamp;
	if (inside_lon) {
		qx_clamp = px;
	} else if (s.wraps) {
		double d1 = s.minx - px;
		double d2 = px - s.maxx;
		qx_clamp = (d1 <= d2) ? s.minx : s.maxx;
	} else {
		double d_min = std::fabs(px - s.minx);
		if (d_min > M_PI) d_min = M_2PI - d_min;
		double d_max = std::fabs(px - s.maxx);
		if (d_max > M_PI) d_max = M_2PI - d_max;
		qx_clamp = (d_min <= d_max) ? s.minx : s.maxx;
	}

	// Top and bottom edges (constant lat).
	double D = hav_rad(px, py, qx_clamp, s.maxy, cos_py);
	double D_bot = hav_rad(px, py, qx_clamp, s.miny, cos_py);
	if (D_bot < D) D = D_bot;

	// Left and right edges (constant lon): also test analytic optimum qy.
	const double half_pi = M_PI * 0.5;
	double qx_edge[2] = { s.minx, s.maxx };
	for (int e = 0; e < 2; e++) {
		double qx = qx_edge[e];
		double D_min_y = hav_rad(px, py, qx, s.miny, cos_py);
		double D_max_y = hav_rad(px, py, qx, s.maxy, cos_py);
		double D_edge = std::min(D_min_y, D_max_y);
		double dlon = px - qx;
		if (dlon >  M_PI) dlon -= M_2PI;
		if (dlon < -M_PI) dlon += M_2PI;
		double A = sin(py);
		double B = cos_py * cos(dlon);
		if (B > 0.0) {
			double qy_opt = atan2(A, B);
			if (qy_opt > -half_pi && qy_opt < half_pi && qy_opt > s.miny && qy_opt < s.maxy) {
				double D_opt = hav_rad(px, py, qx, qy_opt, cos_py);
				if (D_opt < D_edge) D_edge = D_opt;
			}
		}
		if (D_edge < D) D = D_edge;
	}

	return D * EARTH_R;
}

// Push every line segment of every part (and every hole) of `p` into `out`
// with its bbox precomputed. `p` is taken non-const because several SpatVector
// accessors (size(), nHoles(), parts indexing) are not declared const.
static void collect_segments(SpatVector &p, std::vector<LLSeg> &out) {
	size_t total = 0;
	for (size_t g = 0; g < p.size(); g++) {
		for (size_t h = 0; h < p.geoms[g].size(); h++) {
			SpatPart &part = p.geoms[g].parts[h];
			if (part.x.size() > 1) total += part.x.size() - 1;
			for (size_t k = 0; k < part.nHoles(); k++) {
				if (part.holes[k].x.size() > 1) total += part.holes[k].x.size() - 1;
			}
		}
	}
	out.clear();
	out.reserve(total);

	for (size_t g = 0; g < p.size(); g++) {
		for (size_t h = 0; h < p.geoms[g].size(); h++) {
			SpatPart &part = p.geoms[g].parts[h];
			for (size_t i = 0; i + 1 < part.x.size(); i++) {
				LLSeg s;
				compute_seg_bbox(part.x[i], part.y[i],
				                 part.x[i+1], part.y[i+1], s);
				out.push_back(s);
			}
			for (size_t k = 0; k < part.nHoles(); k++) {
				SpatHole &hole = part.holes[k];
				for (size_t i = 0; i + 1 < hole.x.size(); i++) {
					LLSeg s;
					compute_seg_bbox(hole.x[i], hole.y[i], hole.x[i+1], hole.y[i+1], s);
					out.push_back(s);
				}
			}
		}
	}
}

} // anonymous namespace


SpatRaster SpatRaster::distance_vector_lonlat_fast(SpatVector p, std::string unit, const std::string& method, SpatOptions &opt) {

	SpatRaster out = geometry();

	if (source[0].srs.is_empty() && p.srs.is_empty()) {
		out.addWarning("unknown CRSs. Results can be wrong");
	} else if (!source[0].srs.is_same(p.srs, false)) {
		out.setError("CRSs do not match");
		return out;
	}
	if (p.empty()) {
		out.setError("no locations to compute distance from");
		return out;
	}
	if ((unit != "m") && (unit != "km")) {
		out.setError("invalid unit. Must be 'm' or 'km'");
		return out;
	}
	if ((method != "haversine") && (method != "cosine")) {
		// callers should only route us here for these methods
		out.setError("internal: distance_vector_lonlat_fast called with unsupported method");
		return out;
	}

	std::string gtype = p.type();
	if ((gtype != "polygons") && (gtype != "lines")) {
		out.setError("internal: distance_vector_lonlat_fast called with non-line/polygon input");
		return out;
	}
	bool is_poly = (gtype == "polygons");

	if (p.nrow() > 1) {
		p = p.aggregate(true);
	}

	std::vector<LLSeg> segs;
	collect_segments(p, segs);
	const size_t nseg = segs.size();
	if (nseg == 0) {
		out.setError("no segments to compute distance from");
		return out;
	}

	const double EARTH_R = 6378137.0;
	const double D2R = 0.017453292519943295;
	const double m_unit = (unit == "km") ? 0.001 : 1.0;

	const size_t nc = ncol();

	std::vector<int64_t> cols(nc);
	std::iota(cols.begin(), cols.end(), 0);
	std::vector<double> xs_deg = xFromCol(cols);   // longitudes (deg) once
	std::vector<double> xs_rad(nc);
	for (size_t c = 0; c < nc; c++) xs_rad[c] = xs_deg[c] * D2R;

	if (!out.writeStart(opt, filenames())) {
		return out;
	}

	for (size_t b = 0; b < out.bs.n; b++) {
		const size_t nrow_blk = out.bs.nrows[b];
		const size_t nblk = nrow_blk * nc;

		std::vector<double> ys_deg(nrow_blk), ys_rad(nrow_blk);
		for (size_t r = 0; r < nrow_blk; r++) {
			ys_deg[r] = yFromRow(out.bs.row[b] + r);
			ys_rad[r] = ys_deg[r] * D2R;
		}

		// One spherical point-in-polygon call per block (for polygons only).
		// Uses great-circle arcs as edges 
		std::vector<int> inside;
		if (is_poly) {
			std::vector<double> all_x(nblk), all_y(nblk);
			for (size_t r = 0; r < nrow_blk; r++) {
				for (size_t c = 0; c < nc; c++) {
					all_x[r*nc + c] = xs_deg[c];
					all_y[r*nc + c] = ys_deg[r];
				}
			}
			inside = p.pointInPolygonGeo(all_x, all_y);
		}

		std::vector<double> d(nblk, std::numeric_limits<double>::infinity());

		auto process_row = [&](size_t r) {
			const double py = ys_rad[r];
			const double cos_py = cos(py);

			// Per-segment latitude-only lower bound, cached for the row.
			// (geodesic distance is at least the meridional latitude difference)
			// used as a quick reject before the more expensive bbox bound.
			std::vector<double> seg_lat_lb(nseg);
			for (size_t s = 0; s < nseg; s++) {
				double dlat = 0.0;
				if (py < segs[s].miny) {
					dlat = segs[s].miny - py;
				} else if (py > segs[s].maxy) {
					dlat = py - segs[s].maxy;
				}
				seg_lat_lb[s] = EARTH_R * dlat;
			}

			const size_t row_off = r * nc;

			for (size_t c = 0; c < nc; c++) {
				const size_t idx = row_off + c;
				if (is_poly && inside[idx]) {
					d[idx] = 0.0;
					continue;
				}
				const double px = xs_rad[c];
				double cur = std::numeric_limits<double>::infinity();

				for (size_t s = 0; s < nseg; s++) {
					if (seg_lat_lb[s] >= cur) continue;
					double lb = bbox_lower_bound_m(px, py, cos_py, segs[s]);
					if (lb >= cur) continue;
					double ds = dist2segment_cos(px, py, segs[s].x1, segs[s].y1, segs[s].x2, segs[s].y2, EARTH_R);
					if (ds < cur) cur = ds;
				}
				d[idx] = cur;
			}
		};

#if defined(USE_TBB)
		if (opt.parallel) {
			terra_parallel_for(opt, tbb::blocked_range<size_t>(0, nrow_blk),
				[&](const tbb::blocked_range<size_t>& range) {
					for (size_t r = range.begin(); r != range.end(); r++) {
						process_row(r);
					}
				});
		} else {
			for (size_t r = 0; r < nrow_blk; r++) process_row(r);
		}
#else
		for (size_t r = 0; r < nrow_blk; r++) process_row(r);
#endif

		if (m_unit != 1.0) {
			for (double &v : d) v *= m_unit;
		}
		if (!out.writeBlock(d, b)) return out;
	}

	out.writeStop();
	return out;
}


// ----------------------------------------------------------------------------
// Fast path for distance(SpatRaster, SpatVector) for lon/lat polygons or lines
// using the ellipsoidal "geo" method.
//
// Same structure as the haversine/cosine fast path, with these twists:
//   * ellipsoid (a, f) is extracted from the CRS (WGS84 fallback)
//   * the per-segment latitude-only lower bound uses R_min = a*(1-e^2), the
//     minimum meridional radius of curvature, which is a true LB on geodesic
//     distance given |dlat|.
//   * the per-segment bbox lower bound (spherical, computed on R=a) is
//     multiplied by (1-e^2) to guarantee a true LB on geodesic distance.
//     (1-e^2) is ~0.9933 for WGS84, so the prune loses <0.7% of its sharpness
//     relative to the spherical paths.
// ----------------------------------------------------------------------------


SpatRaster SpatRaster::distance_vector_lonlat_fast_geo(SpatVector p, std::string unit, SpatOptions &opt) {

	SpatRaster out = geometry();

	if (source[0].srs.is_empty() && p.srs.is_empty()) {
		out.addWarning("unknown CRSs. Results can be wrong");
	} else if (!source[0].srs.is_same(p.srs, false)) {
		out.setError("CRSs do not match");
		return out;
	}
	if (p.empty()) {
		out.setError("no locations to compute distance from");
		return out;
	}
	if ((unit != "m") && (unit != "km")) {
		out.setError("invalid unit. Must be 'm' or 'km'");
		return out;
	}

	std::string gtype = p.type();
	if ((gtype != "polygons") && (gtype != "lines")) {
		out.setError("internal: distance_vector_lonlat_fast_geo called with non-line/polygon input");
		return out;
	}
	bool is_poly = (gtype == "polygons");

	if (p.nrow() > 1) {
		p = p.aggregate(true);
	}

	std::vector<LLSeg> segs;
	collect_segments(p, segs);
	const size_t nseg = segs.size();
	if (nseg == 0) {
		out.setError("no segments to compute distance from");
		return out;
	}

	const double D2R = 0.017453292519943295;
	const double m_unit = (unit == "km") ? 0.001 : 1.0;

	// Ellipsoid params from source CRS (WGS84 fallback). Falls back to WGS84
	// when source has empty SRS; if source empty but vector has a valid SRS,
	// try that too.
	double geo_a, geo_f;
	if (!source[0].srs.is_empty()) {
		geo_ellipsoid_from_wkt(source[0].srs.wkt, geo_a, geo_f);
	} else {
		geo_ellipsoid_from_wkt(p.srs.wkt, geo_a, geo_f);
	}
	const double e2 = geo_f * (2.0 - geo_f);
	const double R_min = geo_a * (1.0 - e2);    // meridional radius of curvature (min)
	const double bbox_scale = (1.0 - e2);       // safety factor on spherical-on-a LB

	struct geod_geodesic g_ell;
	geod_init(&g_ell, geo_a, geo_f);

	const size_t nc = ncol();
	std::vector<int64_t> cols(nc);
	std::iota(cols.begin(), cols.end(), 0);
	std::vector<double> xs_deg = xFromCol(cols);
	std::vector<double> xs_rad(nc);
	for (size_t c = 0; c < nc; c++) xs_rad[c] = xs_deg[c] * D2R;

	if (!out.writeStart(opt, filenames())) {
		return out;
	}

	for (size_t b = 0; b < out.bs.n; b++) {
		const size_t nrow_blk = out.bs.nrows[b];
		const size_t nblk = nrow_blk * nc;

		std::vector<double> ys_deg(nrow_blk), ys_rad(nrow_blk);
		for (size_t r = 0; r < nrow_blk; r++) {
			ys_deg[r] = yFromRow(out.bs.row[b] + r);
			ys_rad[r] = ys_deg[r] * D2R;
		}

		std::vector<int> inside;
		if (is_poly) {
			std::vector<double> all_x(nblk), all_y(nblk);
			for (size_t r = 0; r < nrow_blk; r++) {
				for (size_t c = 0; c < nc; c++) {
					all_x[r*nc + c] = xs_deg[c];
					all_y[r*nc + c] = ys_deg[r];
				}
			}
			inside = p.pointInPolygonGeo(all_x, all_y);
		}

		std::vector<double> d(nblk, std::numeric_limits<double>::infinity());

		auto process_row = [&](size_t r) {
			const double py = ys_rad[r];
			const double py_d = ys_deg[r];
			const double cos_py = cos(py);

			// Per-segment latitude-only geodesic lower bound, cached for the row.
			std::vector<double> seg_lat_lb(nseg);
			for (size_t s = 0; s < nseg; s++) {
				double dlat = 0.0;
				if (py < segs[s].miny) dlat = segs[s].miny - py;
				else if (py > segs[s].maxy) dlat = py - segs[s].maxy;
				seg_lat_lb[s] = R_min * dlat;
			}

			const size_t row_off = r * nc;

			for (size_t c = 0; c < nc; c++) {
				const size_t idx = row_off + c;
				if (is_poly && inside[idx]) {
					d[idx] = 0.0;
					continue;
				}
				const double px = xs_rad[c];
				const double px_d = xs_deg[c];
				double cur = std::numeric_limits<double>::infinity();

				for (size_t s = 0; s < nseg; s++) {
					if (seg_lat_lb[s] >= cur) continue;
					double lb = bbox_scale * bbox_lower_bound_m(px, py, cos_py, segs[s]);
					if (lb >= cur) continue;
					double ds = dist2segment_geo_ell(px_d, py_d,
					                                  segs[s].x1 / D2R, segs[s].y1 / D2R,
					                                  segs[s].x2 / D2R, segs[s].y2 / D2R,
					                                  geo_a, g_ell);
					if (ds < cur) cur = ds;
				}
				d[idx] = cur;
			}
		};

#if defined(USE_TBB)
		if (opt.parallel) {
			terra_parallel_for(opt, tbb::blocked_range<size_t>(0, nrow_blk),
				[&](const tbb::blocked_range<size_t>& range) {
					for (size_t r = range.begin(); r != range.end(); r++) {
						process_row(r);
					}
				});
		} else {
			for (size_t r = 0; r < nrow_blk; r++) process_row(r);
		}
#else
		for (size_t r = 0; r < nrow_blk; r++) process_row(r);
#endif

		if (m_unit != 1.0) {
			for (double &v : d) v *= m_unit;
		}
		if (!out.writeBlock(d, b)) return out;
	}

	out.writeStop();
	return out;
}


SpatRaster SpatRaster::distance_vector(SpatVector p, bool rasterize, std::string unit, const std::string& method, SpatOptions &opt) {

	SpatRaster out = geometry();

	if (source[0].srs.is_empty() && p.srs.is_empty()) {
		out.addWarning("unknown CRSs. Results can be wrong");
	} else if (!source[0].srs.is_same(p.srs, false)) {
		out.setError("CRSs do not match");
		return(out);
	}
	if (p.empty()) {
		out.setError("no locations to compute distance from");
		return(out);
	}
	bool lonlat = is_lonlat(); 

	if ((unit != "m") && (unit != "km")) {
		out.setError("invalid unit. Must be 'm' or 'km'");
		return(out);
	}
	if ((method != "geo") && (method != "cosine") && (method != "haversine")) {
		out.setError("invalid method. Must be 'geo', 'haversine' or 'cosine'");
		return(out);
	}

	if (rasterize) {
//SpatRaster SpatRaster::distance_rasterize(SpatVector p, double target, double exclude, std::string unit, const std::string& method, SpatOptions &opt) {
//		double target = NAN;

		double exclude = NAN;

		SpatRaster x;
		SpatOptions ops(opt);
		std::string gtype = p.type();
		bool poly = gtype == "polygons";

		x = out.rasterize(p, "", {1}, NAN, false, "", false, false, false, ops);

		if (!lonlat) {
			return x.distance(NAN, 0, false, unit, false, method, false, -1, opt);
		}

		if (poly) {
			x  = x.edges(false, false, "inner", 8, 0, ops);
			SpatRaster xp = x.replaceValues({0}, {exclude}, 1, false, NAN, false, ops);
			p  = xp.as_points(false, true, false, opt);
		} else {
	//		x = x.edges(false, "inner", 8, NAN, ops);
			p = x.as_points(false, true, false, opt);
		}

		std::vector<std::vector<double>> pxy = p.coordinates();

		if (pxy.empty()) {
			out.setError("no locations to compute from");
			return(out);
		}


		bool setNA = false;
		out = x.distance_crds(pxy[0], pxy[1], method, poly, setNA, unit, -1, opt);

	} else {

		if ((p.type() == "polygons") || (p.type() == "lines")) {

			// Fast path: lon/lat with haversine/cosine/geo method.
			// Set TERRA_DIST_VECTOR_SLOW=1 to bypass for benchmarking.
			if (lonlat) {
				const char *slow = std::getenv("TERRA_DIST_VECTOR_SLOW");
				bool fast_on = (slow == nullptr || slow[0] == '\0' || slow[0] == '0');
				if (fast_on) {
					if ((method == "haversine") || (method == "cosine")) {
						return distance_vector_lonlat_fast(p, unit, method, opt);
					} else if (method == "geo") {
						return distance_vector_lonlat_fast_geo(p, unit, opt);
					}
				}
			}

			if (p.nrow() > 1) {
				p = p.aggregate(true);
			}

			std::vector<double> cells;
			size_t nc = ncol();

			if (!readStart()) {
				out.setError(getError());
				return(out);
			}
			if (!out.writeStart(opt, filenames())) {
				readStop();
				return out;
			}
			for (size_t i = 0; i < out.bs.n; i++) {
				cells.resize(out.bs.nrows[i] * nc) ;
				std::iota(cells.begin(), cells.end(), out.bs.row[i] * nc);
				std::vector<std::vector<double>> rxy = xyFromCell(cells);

				SpatVector pnts;
				pnts.srs = source[0].srs;
				pnts.setPointsGeometry(rxy[0], rxy[1]);
				std::vector<double> d = pnts.distance(p, false, unit, method, false, opt);

				if (!out.writeBlock(d, i)) return out;
			}
			readStop();
			out.writeStop();

		} else {
		//p = p.aggregate(false);
			if ((method != "geo") && (method != "cosine") && (method != "haversine")) {
				out.setError("invalid method. Must be 'geo', 'cosine' or 'haversine'");
				return(out);
			}

			std::vector<std::vector<double>> pxy = p.coordinates();
			SpatOptions ops(opt);
			bool setNA = false;
			out = distance_crds(pxy[0], pxy[1], method, false, setNA, unit, -1, opt);
		}
	}
	return out;

}


SpatRaster SpatRaster::direction_rasterize(SpatVector p, bool from, bool degrees, double target, double exclude, const std::string &method, SpatOptions &opt) {

	SpatRaster out = geometry();

	if (source[0].srs.is_empty() && p.srs.is_empty()) {
		out.addWarning("unknown CRSs. Results can be wrong");
	} else if (!source[0].srs.is_same(p.srs, false)) {
		out.setError("CRSs do not match");
		return(out);
	}
	bool lonlat = is_lonlat(); 

	SpatRaster x;
	SpatOptions ops(opt);
	std::string gtype = p.type();
	bool poly = gtype == "polygons";

	x = out.rasterize(p, "", {1}, NAN, false, "", false, false, false, ops);


	if (poly) {
		x  = x.edges(false, false, "inner", 8, 0, ops);
		SpatRaster xp = x.replaceValues({0}, {exclude}, 1, false, NAN, false, ops);
		p  = xp.as_points(false, true, false, opt);
	} else {
//		x = x.edges(false, "inner", 8, NAN, ops);
		p = x.as_points(false, true, false, opt);
	}

	std::vector<std::vector<double>> pxy = p.coordinates();

	if (pxy.empty()) {
		out.setError("no locations to compute from");
		return(out);
	}
	size_t nc = ncol();
	if (!readStart()) {
		out.setError(getError());
		return(out);
	}

 	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}

	for (size_t i = 0; i < out.bs.n; i++) {
		std::vector<double> v;
		std::vector<double> cells(out.bs.nrows[i] * nc) ;
		std::vector<double> vals;
		vals.resize(out.bs.nrows[i] * nc, NAN);

		std::iota(cells.begin(), cells.end(), out.bs.row[i] * nc);

		x.readBlock(v, out.bs, i);
		if (std::isnan(target)) {
			for (size_t j=0; j<v.size(); j++) {
				if (!std::isnan(v[j])) {
					cells[j] = NAN;
				}
			}
		} else {
			for (size_t j=0; j<v.size(); j++) {
				if (v[j] != target) {
					cells[j] = NAN;
					if (std::isnan(v[j])) {
						vals[j] = NAN;
					}
				}
			}
		}
		std::vector<std::vector<double>> xy = xyFromCell(cells);
		shortDirectPoints(vals, xy[0], xy[1], pxy[0], pxy[1], lonlat, from, degrees, method);
		if (!out.writeBlock(vals, i)) return out;
	}

	out.writeStop();
	readStop();
	return(out);
}


/*
SpatRaster SpatRaster::distance_vector(SpatVector p, std::string unit, SpatOptions &opt) {

	SpatRaster out = geometry();
	if (source[0].srs.wkt == "") {
		out.setError("CRS not defined");
		return(out);
	}
	if (!source[0].srs.is_same(p.srs, false) ) {
		out.setError("CRS does not match");
		return(out);
	}

	bool lonlat = is_lonlat(); 
	double m=1;
//	if (!get_m(m, source[0].srs, lonlat, unit)) {
	if (!source[0].srs.m_dist(m, lonlat, unit)) {

		out.setError("invalid unit");
		return(out);
	}

	if (p.size() == 0) {
		out.setError("no locations to compute distance from");
		return(out);
	}
	p = p.aggregate(false);

//	bool lonlat = is_lonlat(); // m == 0
	size_t nc = ncol();

 	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}
	std::vector<double> cells;

	for (size_t i = 0; i < out.bs.n; i++) {
		double s = out.bs.row[i] * nc;
		cells.resize(out.bs.nrows[i] * nc) ;
		std::iota(cells.begin(), cells.end(), s);
		std::vector<std::vector<double>> xy = xyFromCell(cells);
		SpatVector pv(xy[0], xy[1], points, "");
		pv.srs = source[0].srs;
		std::vector<double> d = p.distance(pv, false, unit);
		if (p.hasError()) {
			out.setError(p.getError());
			out.writeStop();
			return(out);
		}
		if (m != 1) {
			for (double &v : d) v *= m;
		}
		if (!out.writeBlock(d, i)) return out;
	}
	out.writeStop();
	return(out);
}

*/

SpatRaster SpatRaster::distance(double target, double exclude, bool keepNA, std::string unit, bool remove_zero, const std::string method, bool values, double threshold, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if (!hasValues()) {
		out.setError("SpatRaster has no values");
		return out;
	}

//	bool guess_lonlat = false;
//	if (source[0].srs.is_empty() && could_be_lonlat()) {
//		guess_lonlat = true;
//		crswarning = "Guessing that the CRS is lon/lat");
//	}
//	bool as_lonlat = is_lonlat() || guess_lonlat;


	SpatOptions ops(opt);
	size_t nl = nlyr();
	if (nl > 1) {
		std::vector<std::string> nms = getNames();
		if (ops.names.size() == nms.size()) {
			nms = opt.names;
		}
		out.source.resize(nl);
		for (size_t i=0; i<nl; i++) {
			std::vector<size_t> lyr = {i};
			SpatRaster r = subset(lyr, ops);
			ops.names = {nms[i]};
			r = r.distance(target, exclude, keepNA, unit, remove_zero, method, values, threshold, ops);
			out.source[i] = r.source[0];
		}
		if (!opt.get_filename().empty()) {
			out = out.writeRaster(opt);
		}
		if (source[0].srs.is_empty()) {
			out.addWarning("unknown CRS. Results can be wrong");
		} 
		return out;
	}
	if (!(values || is_lonlat())) { // && std::isnan(target) && std::isnan(exclude)) {
		out = proximity(target, exclude, keepNA, unit, false, threshold, remove_zero, opt); 
		if (source[0].srs.is_empty()) {
			out.addWarning("unknown CRS. Results can be wrong");
		} 
		return out;
	}

	// Use the lon/lat fast path for haversine/cosine/geo, unless
	// TERRA_DIST_SLOW is set. The fast path is bit-equivalent (within tiny
	// FP noise) to distance_crds for the same inputs.
	const bool dist_slow_env = []() {
		const char *slow = std::getenv("TERRA_DIST_SLOW");
		return !(slow == nullptr || slow[0] == '\0' || slow[0] == '0');
	}();
	const bool use_fast_lonlat = is_lonlat() && !values && !dist_slow_env
		&& ((method == "haversine") || (method == "cosine"));
	const bool use_fast_lonlat_geo = is_lonlat() && !values && !dist_slow_env
		&& (method == "geo");

	bool setNA = false;
	std::vector<std::vector<double>> p;
	if (!std::isnan(exclude)) {
		SpatRaster x;
		if (std::isnan(target)) {
			x = replaceValues({exclude}, {target}, 1, false, NAN, false, ops);
			x = x.edges(false, false, "inner", 8, 1, ops);
			p = x.as_points_value(1, ops);
			if (p.empty()) {
				return out.init({0}, opt);
			}
			if (values) {
				std::vector<std::vector<double>> vv = extractXY(p[0], p[1], "", false, opt);
				return distance_crds_vals(p[0], p[1], vv[0], method, true, setNA, unit, threshold, opt);				
			} else if (use_fast_lonlat) {
				return distance_crds_lonlat_fast(p[0], p[1], method, true, setNA, unit, threshold, opt);
			} else if (use_fast_lonlat_geo) {
				return distance_crds_lonlat_fast_geo(p[0], p[1], true, setNA, unit, threshold, opt);
			} else {
				return distance_crds(p[0], p[1], method, true, setNA, unit, threshold, opt);
			}
		} else {
			x = replaceValues({exclude, target}, {NAN, NAN}, 1, false, NAN, false, ops);
			x = x.edges(false, false, "inner", 8, 1, ops);
			p = x.as_points_value(1, ops);
			out = replaceValues({NAN, exclude, target}, {target, NAN, NAN}, 1, false, NAN, false, ops);
		}
	} else if (!std::isnan(target)) {
		SpatRaster x = replaceValues({target}, {NAN}, 1, false, NAN, false, ops);
		x = x.edges(false, false, "inner", 8, 0, ops);
		p = x.as_points_value(1, ops);
		out = replaceValues({NAN, target}, {std::numeric_limits<double>::max(), NAN}, 1, false, NAN, false, ops);
		setNA = true;
	} else {
		out = edges(false, false, "inner", 8, 0, ops);
		p = out.as_points_value(1, ops);
	}

	if (p.empty()) {
		out = out.init({0}, opt);
	} else if (values) {
		std::vector<std::vector<double>> vv = extractXY(p[0], p[1], "", false, opt);
		out = out.distance_crds_vals(p[0], p[1], vv[0], method, true, setNA, unit, threshold, opt);				
	} else if (use_fast_lonlat) {
		out = out.distance_crds_lonlat_fast(p[0], p[1], method, true, setNA, unit, threshold, opt);
	} else if (use_fast_lonlat_geo) {
		out = out.distance_crds_lonlat_fast_geo(p[0], p[1], true, setNA, unit, threshold, opt);
	} else {
		out = out.distance_crds(p[0], p[1], method, true, setNA, unit, threshold, opt);
	}
	if (source[0].srs.is_empty()) {
		out.addWarning("unknown CRS. Results can be wrong");
	}
	return out;
}



SpatRaster SpatRaster::direction(bool from, bool degrees, double target, double exclude, const std::string &method, SpatOptions &opt) {
	SpatRaster out = geometry(1);
	if (!hasValues()) {
		out.setError("SpatRaster has no values");
		return out;
	}

	SpatOptions ops(opt);
	size_t nl = nlyr();
	if (nl > 1) {
		out.source.resize(nl);
		std::vector<std::string> nms = getNames();
		if (ops.names.size() == nms.size()) {
			nms = opt.names;
		}
		for (size_t i=0; i<nl; i++) {
			std::vector<size_t> lyr = {i};
			SpatRaster r = subset(lyr, ops);
			ops.names = {nms[i]};
			r = r.direction(from, degrees, target, exclude, method, ops);
			out.source[i] = r.source[0];
		}
		if (!opt.get_filename().empty()) {
			out = out.writeRaster(opt);
		}
		return out;
	}

	if (!std::isnan(exclude)) {
		SpatOptions xopt(opt);
		SpatRaster x = replaceValues({exclude}, {NAN}, 1, false, NAN, false, xopt);
		out = x.edges(false, false, "inner", 8, target, ops);
	} else {
		out = edges(false, false, "inner", 8, target, ops);
	}
	SpatVector p = out.as_points(false, true, false, opt);
	if (p.empty()) {
		out.setError("no cells to compute direction from or to");
		return(out);
	}
	return direction_rasterize(p, from, degrees, target, exclude, method, opt);
}








std::vector<double> broom_dist_planar(std::vector<double> &v, std::vector<double> &above, std::vector<double> res, size_t nr, size_t nc, double lindist) {

	double dx = res[0] * lindist;
	double dy = res[1] * lindist;
	double dxy = sqrt(dx * dx + dy *dy);

	std::vector<double> dist(v.size(), 0);

    //left to right
	//first cell, no cell left of it
	if ( std::isnan(v[0]) ) {
		dist[0] = above[0] + dy;
	}
	//first row, no row above it, use "above"
	for (size_t i=1; i<nc; i++) {
		if (std::isnan(v[i])) {
			dist[i] = std::min(std::min(above[i] + dy, above[i-1] + dxy), dist[i-1] + dx);
		}
	}

	//other rows
	for (size_t r=1; r<nr; r++) {
		size_t start=r*nc;
		if (std::isnan(v[start])) {
			dist[start] = dist[start-nc] + dy;
		}

		size_t end = start+nc;
		for (size_t i=(start+1); i<end; i++) {
			if (std::isnan(v[i])) {
				dist[i] = std::min(std::min(dist[i-1] + dx, dist[i-nc] + dy), dist[i-nc-1] + dxy);
			}
		}
	}

	//right to left
	//first cell
	if ( std::isnan(v[nc-1])) {
		dist[nc-1] = std::min(dist[nc-1], above[nc-1] + dy);
	}

	// other cells on first row
	for (int i=(nc-2); i > -1; i--) {
		if (std::isnan(v[i])) {
			dist[i] = std::min(std::min(std::min(dist[i+1] + dx, above[i+1] + dxy), above[i] + dy), dist[i]);
		}
	}

	// other rows
	for (size_t r=1; r<nr; r++) {
		size_t start=(r+1)*nc-1;
		if (std::isnan(v[start])) {
			dist[start] = std::min(dist[start], dist[start-nc] + dy);
		}
		for (size_t i=start-1; i>=(r*nc); i--) {
			if (std::isnan(v[i])) {
				dist[i] = std::min(std::min(std::min(dist[i], dist[i+1] + dx), dist[i-nc] + dy), dist[i-nc+1] + dxy);
			}
		}
	}
	size_t off = (nr-1) * nc;
	above = std::vector<double>(dist.begin()+off, dist.end());
	return dist;
}

/*
inline double minNArm(const double &a, const double &b) {
	if (std::isnan(a)) return b;
	if (std::isnan(b)) return a;
	return std::min(a, b);
}
*/

inline void DxDxy(const double &lat, const size_t &row, const double &xres, double yres, const double &dir, const double &scale, double &dx,  double &dy, double &dxy) {
	double rlat = lat + (double)row * yres * dir;
	dx  = distance_lonlat(0, rlat, xres, rlat) / scale;
	yres *= -dir;
	dy  = distance_lonlat(0, rlat, 0, rlat+yres);
	dxy = distance_lonlat(0, rlat, xres, rlat+yres);
	dy = std::isnan(dy) ? std::numeric_limits<double>::infinity() : dy / scale;
	dxy = std::isnan(dxy) ? std::numeric_limits<double>::infinity() : dxy / scale;
}


void broom_dist_geo(std::vector<double> &dist, std::vector<double> &v, std::vector<double> &above, std::vector<double> res, size_t nr, size_t nc, double lat, double latdir, double scale, bool npole, bool spole) {

	double dx, dy, dxy;
	//top to bottom
    //left to right
	DxDxy(lat, 0, res[0], res[1], latdir, scale, dx, dy, dxy);
	//first cell, no cell left of it
	if ( std::isnan(v[0]) ) {
		dist[0] = std::min(above[0] + dy, dist[0]);
	}
	//first row, no row above it, use "above"
	for (size_t i=1; i<nc; i++) {
		if (std::isnan(v[i])) {
			dist[i] = std::min(std::min(std::min(above[i] + dy, above[i-1] + dxy), dist[i-1] + dx), dist[i]);
		}
	}
	if (npole) {
		double minp = *std::min_element(dist.begin(), dist.begin()+nc);
		minp += dy;
		for (size_t i=0; i<nc; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}

	//other rows
	for (size_t r=1; r<nr; r++) {
		DxDxy(lat, r, res[0], res[1], latdir, scale, dx, dy, dxy);
		size_t start=r*nc;
		if (std::isnan(v[start])) {
			dist[start] = std::min(dist[start], dist[start-nc] + dy);
		}
		size_t end = start+nc;
		for (size_t i=(start+1); i<end; i++) {
			if (std::isnan(v[i])) {
				dist[i] = std::min(dist[i], std::min(std::min(dist[i-1] + dx, dist[i-nc] + dy), dist[i-nc-1] + dxy));
			}
		}
	}
	if (spole) {
		double minp = *std::min_element(dist.end()-nc, dist.end());
		minp += dy;
		size_t ds = dist.size();
		for (size_t i=ds-nc; i<ds; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}

	//right to left
	DxDxy(lat, 0, res[0], res[1], latdir, scale, dx, dy, dxy);
	 //first cell
	if ( std::isnan(v[nc-1])) {
		dist[nc-1] = std::min(dist[nc-1], above[nc-1] + dy);
	}

	// other cells on first row
	for (int i=(nc-2); i > -1; i--) {
		if (std::isnan(v[i])) {
			dist[i] = std::min(std::min(std::min(dist[i+1] + dx, above[i+1] + dxy), above[i] + dy), dist[i]);
		}
	}
	if (npole) {
		double minp = *std::min_element(dist.begin(), dist.begin()+nc);
		minp += dy;
		for (size_t i=0; i<nc; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}
	// other rows
	for (size_t r=1; r<nr; r++) {
		DxDxy(lat, r, res[0], res[1], latdir, scale, dx, dy, dxy);

		size_t start=(r+1)*nc-1;
		if (std::isnan(v[start])) {
			dist[start] = std::min(dist[start], dist[start-nc] + dy);
		}
		for (size_t i=start-1; i>=(r*nc); i--) {
			if (std::isnan(v[i])) {
				dist[i] = std::min(std::min(std::min(dist[i], dist[i+1] + dx), dist[i-nc] + dy), dist[i-nc+1] + dxy);
			}
		}
	}
	if (spole) {
		double minp = *std::min_element(dist.end()-nc, dist.end());
		minp += dy;
		size_t ds = dist.size();
		for (size_t i=ds-nc; i<ds; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}

	size_t off = (nr-1) * nc;
	above = std::vector<double>(dist.begin()+off, dist.end());
}


void broom_dist_geo_global(std::vector<double> &dist, std::vector<double> &v, std::vector<double> &above, std::vector<double> res, size_t nr, size_t nc, double lat, double latdir, double scale, bool npole, bool spole) {

//	double dy = distance_lonlat(0, 0, 0, res[1]);
	double dx, dy, dxy;
	size_t stopnc = nc - 1;
	//top to bottom
    //left to right
	DxDxy(lat, 0, res[0], res[1], latdir, scale, dx, dy, dxy);
	//first cell, no cell left of it
	if ( std::isnan(v[0]) ) {
		dist[0] = std::min(std::min(std::min(above[0] + dy, above[stopnc] + dxy), dist[stopnc] + dx), dist[0]);
	}
	//first row, no row above it, use "above"
	for (size_t i=1; i<nc; i++) {
		if (std::isnan(v[i])) {
			dist[i] = std::min(std::min(std::min(above[i] + dy, above[i-1] + dxy),
								dist[i-1] + dx), dist[i]);
		}
	}
	if (npole) {
		double minp = *std::min_element(dist.begin(), dist.begin()+nc);
		minp += dy;
		for (size_t i=0; i<nc; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}

	//other rows
	for (size_t r=1; r<nr; r++) {
		DxDxy(lat, r, res[0], res[1], latdir, scale, dx, dy, dxy);
		size_t start=r*nc;
		if (std::isnan(v[start])) {
			dist[start] = std::min(std::min(std::min(dist[start-nc] + dy, dist[start-1] + dxy),
									dist[start+stopnc] + dx), dist[start]);
		}

		size_t end = start+nc;
		for (size_t i=(start+1); i<end; i++) {
			if (std::isnan(v[i])) {
				dist[i] = std::min(std::min(std::min(dist[i-1] + dx, dist[i-nc] + dy),
						dist[i-nc-1] + dxy), dist[i]);
			}
		}
	}

	if (spole) {
		double minp = *std::min_element(dist.end()-nc, dist.end());
		minp += dy;
		size_t ds = dist.size();
		for (size_t i=ds-nc; i<ds; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}

	//right to left
	DxDxy(lat, 0, res[0], res[1], latdir, scale, dx, dy, dxy);
	 //first cell
	if ( std::isnan(v[stopnc])) {
		dist[stopnc] = std::min(std::min(std::min(dist[stopnc], above[stopnc] + dy), above[0] + dxy), dist[0] + dx);
	}

	// other cells on first row
	for (int i=(nc-2); i > -1; i--) {
		if (std::isnan(v[i])) {
			dist[i] = std::min(std::min(std::min(dist[i+1] + dx, above[i+1] + dxy),
						above[i] + dy), dist[i]);
		}
	}
	if (npole) {
		double minp = *std::min_element(dist.begin(), dist.begin()+nc);
		minp += dy;
		for (size_t i=0; i<nc; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}

	// other rows
	for (size_t r=1; r<nr; r++) {
		DxDxy(lat, r, res[0], res[1], latdir, scale, dx, dy, dxy);

		size_t start=(r+1)*nc-1;
		if (std::isnan(v[start])) {
			//dist[start] = std::min(dist[start], dist[start-nc] + dy);
			dist[start] = std::min(std::min(std::min(dist[start], dist[start-nc] + dy), dist[start-nc-stopnc] + dxy)
							, dist[start-stopnc] + dx);
		}
		size_t end = r*nc;
		for (size_t i=start-1; i>=end; i--) {
			if (std::isnan(v[i])) {
			//	dist[i] = std::min(std::min(std::min(dist[i], dist[i+1] + dx), dist[i-nc] + dy), dist[i-nc+1] + dxy);
				dist[i] = std::min(std::min(std::min(dist[i], dist[i+1] + dx), dist[i-nc] + dy),
						dist[i-nc+1] + dxy);
			}
		}
	}
	if (spole) {
		double minp = *std::min_element(dist.end()-nc, dist.end());
		minp += dy;
		size_t ds = dist.size();
		for (size_t i=ds-nc; i<ds; i++) {
			dist[i] = std::min(dist[i], minp);
		}
	}

	size_t off = (nr-1) * nc;
	above = std::vector<double>(dist.begin()+off, dist.end());

}


SpatRaster SpatRaster::gridDistance(double m, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if (!hasValues()) {
		out.setError("cannot compute distance for a raster with no values");
		return out;
	}

	SpatOptions ops(opt);
	size_t nl = nlyr();
	if (nl > 1) {
		out.source.resize(nl);
		for (size_t i=0; i<nl; i++) {
			std::vector<size_t> lyr = {i};
			SpatRaster r = subset(lyr, ops);
			r = r.gridDistance(m, ops);
			out.source[i] = r.source[0];
		}
		if (!opt.get_filename().empty()) {
			out = out.writeRaster(opt);
		}
		return out;
	}

	SpatRaster first = out.geometry();

	std::vector<double> res = resolution();
	size_t nc = ncol();

	if (source[0].srs.is_empty()) {
		out.addWarning("unknown CRS. Results can be wrong");
	}

	bool lonlat = is_lonlat();
    std::vector<double> d, v;
	std::vector<double> above(nc, std::numeric_limits<double>::infinity());

	if (!readStart()) {
		out.setError(getError());
		return(out);
	}

	if (lonlat) {
		bool global = is_global_lonlat();
		int polar = ns_polar();
		bool npole = (polar == 1) || (polar == 2);
		bool spole = (polar == -1) || (polar == 2);
		SpatRaster second = first;
		if (!first.writeStart(ops, filenames())) { return first; }
		for (size_t i = 0; i < first.bs.n; i++) {
			readBlock(v, first.bs, i);
			d.resize(v.size(), std::numeric_limits<double>::infinity());
			for (size_t j=0; j<d.size(); j++) {
				if (!std::isnan(v[j])) d[j] = 0;
			}
			double lat = yFromRow(first.bs.row[i]);
			bool np = (i==0) && npole;
			bool sp = (i==first.bs.n-1) && spole;
			if (global) {
				broom_dist_geo_global(d, v, above, res, first.bs.nrows[i], nc, lat, -1, m, np, sp);
			} else {
				broom_dist_geo(d, v, above, res, first.bs.nrows[i], nc, lat, -1, m, np, sp);
			}
			if (!first.writeValues(d, first.bs.row[i], first.bs.nrows[i])) return first;
		}
		first.writeStop();

		if (!first.readStart()) {
			readStop();
			return(first);
		}
		if (!second.writeStart(ops, filenames())) {
			readStop();
			return second;
		}
		above = std::vector<double>(ncol(), std::numeric_limits<double>::infinity());
		for (int i = second.bs.n; i>0; i--) {
			readBlock(v, second.bs, i-1);
			first.readBlock(d, second.bs, i-1);
			std::reverse(v.begin(), v.end());
			std::reverse(d.begin(), d.end());
			double lat = yFromRow(second.bs.row[i-1] + second.bs.nrows[i-1] - 1);
			bool sp = (i==1) && spole; //! reverse order
			bool np = (i==(int)second.bs.n) && npole;
			if (global) {
				broom_dist_geo_global(d, v, above, res, second.bs.nrows[i-1], nc, lat, 1, m, np, sp);
			} else {
				broom_dist_geo(d, v, above, res, second.bs.nrows[i-1], nc, lat, 1, m, np, sp);
			}
			std::reverse(d.begin(), d.end());
			if (!second.writeValuesRect(d, second.bs.row[i-1], second.bs.nrows[i-1], 0, nc)) return second;
		}
		second.writeStop();

		if (!second.readStart()) {
			readStop();
			return(second);
		}
		if (!out.writeStart(opt, filenames())) {
			readStop();
			return out;
		}
		above = std::vector<double>(ncol(), std::numeric_limits<double>::infinity());
		for (size_t i = 0; i < out.bs.n; i++) {
			readBlock(v, out.bs, i);
			second.readBlock(d, out.bs, i);
			double lat = yFromRow(first.bs.row[i]);
			bool np = (i==0) && npole;
			bool sp = (i==out.bs.n-1) && spole;
			if (global) {
				broom_dist_geo_global(d, v, above, res, out.bs.nrows[i], nc, lat, -1, m, np, sp);
			} else {
				broom_dist_geo(d, v, above, res, out.bs.nrows[i], nc, lat, -1, m, np, sp);
			}
			if (!out.writeValues(d, out.bs.row[i], out.bs.nrows[i])) return out;
		}
		second.readStop();
		out.writeStop();
		readStop();

	} else {
		double scale = source[0].srs.to_meter() / m;
		scale = std::isnan(scale) ? 1 : scale;

		if (!first.writeStart(ops, filenames())) { return first; }
		std::vector<double> vv;
		for (size_t i = 0; i < first.bs.n; i++) {
			readBlock(v, first.bs, i);
			d = broom_dist_planar(v, above, res, first.bs.nrows[i], nc, scale);
			if (!first.writeValues(d, first.bs.row[i], first.bs.nrows[i])) return first;
		}
		first.writeStop();

		if (!first.readStart()) {
			out.setError(first.getError());
			return(out);
		}
		above = std::vector<double>(ncol(), std::numeric_limits<double>::infinity());
		if (!out.writeStart(opt, filenames())) {
			readStop();
			return out;
		}
		for (int i = out.bs.n; i>0; i--) {
			readBlock(v, out.bs, i-1);
			std::reverse(v.begin(), v.end());
			d = broom_dist_planar(v, above, res, out.bs.nrows[i-1], nc, scale);
			first.readBlock(vv, out.bs, i-1);
			std::transform(d.rbegin(), d.rend(), vv.begin(), vv.begin(), [](double a, double b) {return std::min(a,b);});
			if (!out.writeValuesRect(vv, out.bs.row[i-1], out.bs.nrows[i-1], 0, nc)) return out;
		}
		out.writeStop();
		readStop();
	}
	return(out);
}


std::vector<double> do_edge(const std::vector<double> &d, const size_t nrow, const size_t ncol, const bool classes, const bool internal, const bool inner, const unsigned dirs, double falseval) {

	size_t n = nrow * ncol;
	std::vector<double> val(n, falseval);

	int r[8] = { -1,0,0,1 , -1,-1,1,1};
	int c[8] = { 0,-1,1,0 , -1,1,-1,1};

	if (!classes) {
		if (inner) {
			for (size_t i = 1; i < (nrow-1); i++) {
				for (size_t j = 1; j < (ncol-1); j++) {
					size_t cell = i*ncol+j;
					val[cell] = NAN;
					if ( !std::isnan(d[cell])) {
						val[cell] = falseval;
						for (size_t k=0; k<dirs; k++) {
							if ( std::isnan(d[cell + r[k] * ncol + c[k]])) {
								val[cell] = 1;
								break;
							}
						}
					}
				}
			}
		} else { //outer
			for (size_t i = 1; i < (nrow-1); i++) {
				for (size_t j = 1; j < (ncol-1); j++) {
					size_t cell = i*ncol+j;
					val[cell] = falseval;
					if (std::isnan(d[cell])) {
						val[cell] = NAN;
						for (size_t k=0; k < dirs; k++) {
							if ( !std::isnan(d[cell+ r[k] * ncol + c[k] ])) {
								val[cell] = 1;
								break;
							}
						}
					}
				}
			}
		}
	} else { // by class
		if (internal) { // no difference between inner and outer
			for (size_t i = 1; i < (nrow-1); i++) {
				for (size_t j = 1; j < (ncol-1); j++) {
					size_t cell = i*ncol+j;
					double test = d[cell];
					if (std::isnan(test)) {
						val[cell] = NAN;
					} else {
						val[cell] = falseval;
						for (size_t k=0; k<dirs; k++) {
							double v = d[cell+r[k]*ncol +c[k]];
							if (!std::isnan(v) && (test != v)) {
								val[cell] = 1;
								break;																
							}
						}
					}
				}
			}
		} else { // not internal
			if (inner) {
				for (size_t i = 1; i < (nrow-1); i++) {
					for (size_t j = 1; j < (ncol-1); j++) {
						size_t cell = i*ncol+j;
						double test = d[cell];
						if (std::isnan(test)) {
							val[cell] = NAN;
						} else {
							val[cell] = falseval;
							for (size_t k=0; k<dirs; k++) {
								double v = d[cell+r[k]*ncol +c[k]];
								if (std::isnan(v) || (test != v)) {
									val[cell] = 1;
									break;																
								}
							}
						}
					}
				}
			} else {
				for (size_t i = 1; i < (nrow-1); i++) {
					for (size_t j = 1; j < (ncol-1); j++) {
						size_t cell = i*ncol+j;
						double test = d[cell+r[0]*ncol+c[0]];
						val[cell] = std::isnan(test) ? NAN : falseval;
						for (size_t k=1; k<dirs; k++) {
							double v = d[cell+r[k]*ncol +c[k]];
							if (std::isnan(test)) {
								if (!std::isnan(v)) {
									val[cell] = 1;
									break;
								}
							} else if (test != v) {
								val[cell] = 1;
								break;
							}
						}
					}
				}
			}
		}
	}
	return(val);
}



void addrowcol(std::vector<double> &v, size_t nr, size_t nc, bool rowbefore, bool rowafter, bool cols) {

	if (rowbefore) {
		v.insert(v.begin(), v.begin(), v.begin()+nc);
		nr++;
	}
	if (rowafter) {
		v.insert(v.end(), v.end()-nc, v.end());
		nr++;
	}
	if (cols) {
		for (size_t i=0; i<nr; i++) {
			size_t j = i*(nc+2);
			v.insert(v.begin()+j+nc, v[j+nc-1]);
			v.insert(v.begin()+j, v[j]);
		}
	}
}


void striprowcol(std::vector<double> &v, size_t nr, size_t nc, bool rows, bool cols) {
	if (rows) {
		v.erase(v.begin(), v.begin()+nc);
		v.erase(v.end()-nc, v.end());
		nr -= 2;
	}
	if (cols) {
		nc -= 2;
		for (size_t i=0; i<nr; i++) {
			size_t j = i*nc;
			v.erase(v.begin()+j);
			v.erase(v.begin()+j+nc);
		}
	}
}


SpatRaster SpatRaster::edges(bool classes, bool internal, std::string type, unsigned directions, double falseval, SpatOptions &opt) {

	SpatRaster out = geometry();
	if (nlyr() > 1) {
		std::vector<size_t> lyr = {0};
		SpatOptions ops(opt);
		out = subset(lyr, ops);
		out = out.edges(classes, internal, type, directions, falseval, opt);
		out.addWarning("boundary detection is only done for the first layer");
		return out;
	}
	if (!hasValues()) {
		out.setError("SpatRaster has no values");
		return out;
	}


	if ((directions != 4) && (directions != 8)) {
		out.setError("directions should be 4 or 8");
		return(out);
	}
	if ((type != "inner") && (type != "outer")) {
		out.setError("directions should be 'inner' or 'outer'");
		return(out);
	}
	bool inner = type == "inner";

	size_t nc = ncol();
	size_t nr = nrow();


	if (!readStart()) {
		out.setError(getError());
		return(out);
	}

	opt.minrows = 2;
 	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}

	for (size_t i = 0; i < out.bs.n; i++) {
		std::vector<double> v;
		//bool before = false;
		//bool after = false;
		if (i == 0) {
			if (out.bs.n == 1) {
				readValues(v, out.bs.row[i], out.bs.nrows[i], 0, nc);
				addrowcol(v, nr, nc, true, true, true);
			} else {
				readValues(v, out.bs.row[i], out.bs.nrows[i]+1, 0, nc);
				addrowcol(v, nr, nc, true, false, true);
				//after = true;
			}
		} else {
			//before = true;
			if (i == out.bs.n) {
				readValues(v, out.bs.row[i]-1, out.bs.nrows[i]+1, 0, nc);
				addrowcol(v, nr, nc, false, true, true);
			} else {
				readValues(v, out.bs.row[i]-1, out.bs.nrows[i]+2, 0, nc);
				addrowcol(v, nr, nc, false, false, true);
				//after = true;
			}
		}
		//before, after,
		std::vector<double> vv = do_edge(v, out.bs.nrows[i]+2, nc+2, classes, internal, inner, directions, falseval);
		striprowcol(vv, out.bs.nrows[i]+2, nc+2, true, true);
		if (!out.writeBlock(vv, i)) return out;
	}
	out.writeStop();
	readStop();

	return(out);
}



SpatRaster SpatRaster::buffer(double d, double background, bool include, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if (!hasValues()) {
		out.setError("SpatRaster has no values");
		return out;
	}

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

	if (background == 1) {
		out.setError("the background value cannot be 1");
		return out;
	}

	SpatOptions ops(opt);
	size_t nl = nlyr();
	if (nl > 1) {
		std::vector<std::string> nms = getNames();
		if (ops.names.size() == nms.size()) {
			nms = opt.names;
		}
		out.source.resize(nl);
		for (size_t i=0; i<nl; i++) {
			std::vector<size_t> lyr = {i};
			SpatRaster r = subset(lyr, ops);
			ops.names = {nms[i]};
			r = r.buffer(d, background, include, ops);
			out.source[i] = r.source[0];
		}
		if (!opt.get_filename().empty()) {
			out = out.writeRaster(opt);
		}
		return out;
	}

	if (source[0].srs.is_empty()) {
		out.addWarning("unknown CRS. Results can be wrong");
	}

	if (!is_lonlat()) {
		SpatOptions opt2;
		if (include) {
			opt2 = opt;			
		} else {
			opt2 = ops;
		}
		if (!std::isnan(background)) {
			out = proximity(NAN, NAN, false, "", true, d, true, ops);
			if (background == 0) {
				out = out.isnotnan(false, opt2);
			} else {
				out = out.replaceValues({NAN}, {background}, 1, false, NAN, false, opt2);
			}
		} else {
			out = proximity(NAN, NAN, false, "", true, d, true, opt2);
		}
		if (!include) {
			out = out.mask(*this, true, NAN, NAN, opt);						
		}
	} else {
		SpatRaster e = edges(false, false, "inner", 8, NAN, ops);
		SpatVector p = e.as_points(false, true, false, ops);
		p = p.buffer({d}, 10, "", "", NAN, false);
		p = p.aggregate(true);
		out = out.rasterize(p, "", {1}, background, false, "", false, false, true, ops);
		if (background == 0) {
			out.setValueType(3);
		}
		if (include) {
			out = out.mask(*this, true, NAN, 1, opt);
		} else {
			out = out.mask(*this, true, NAN, NAN, opt);			
		}
		//out = out.disdir_vector_rasterize(p, false, true, false, false, NAN, NAN, "m", ops);
		//out = out.arith(d, "<=", false, opt);
	}
	if (source[0].srs.is_empty()) {
		out.addWarning("unknown CRS. Results may be wrong");
	} 
	return out;
}



SpatRaster SpatRaster::rst_area(bool mask, std::string unit, bool transform, int rcmax, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if (out.source[0].srs.wkt.empty()) {
		out.addWarning("unknown CRS. Results can be wrong");
		transform = false;
	}

	std::vector<std::string> f {"m", "km", "ha"};
	if (std::find(f.begin(), f.end(), unit) == f.end()) {
		out.setError("invalid unit");
		return out;
	}

	if (opt.names.empty()) {
		opt.names = {"area"};
	}
	bool lonlat = is_lonlat();

	SpatOptions mopt(opt);
	if (mask) {
		if (!hasValues()) {
			mask = false;
		} else {
			mopt.filenames = opt.filenames;
			opt.filenames = {""};
		}
	}

	SpatOptions xopt(mopt);

	if (lonlat) {
		bool disagg = false;
		SpatExtent extent = getExtent();
		if ((out.ncol() == 1) && ((extent.xmax - extent.xmin) > 180)) {
			disagg = true;
			std::vector<size_t> fact = {1,2};
			out = out.disaggregate(fact, xopt);
		}
		SpatExtent e = {extent.xmin, extent.xmin+out.xres(), extent.ymin, extent.ymax};
		SpatRaster onecol = out.crop(e, "near", false, xopt);
		SpatVector p = onecol.as_polygons(false, false, false, false, false, 0, xopt);
		if (p.hasError()) {
			out.setError(p.getError());
			return out;
		}
		std::vector<double> a = p.area(unit, true, {});
		size_t nc = out.ncol();
		if (disagg) {
			if (!out.writeStart(xopt, filenames())) { return out; }
		} else {
			if (!out.writeStart(opt, filenames())) { return out; }
		}
		for (size_t i = 0; i < out.bs.n; i++) {
			std::vector<double> v;
			v.reserve(out.bs.nrows[i] * nc);
			size_t r = out.bs.row[i];
			for (size_t j=0; j<out.bs.nrows[i]; j++) {
				v.insert(v.end(), nc, a[r]);
				r++;
			}
			if (!out.writeBlock(v, i)) return out;
		}
		out.writeStop();
		if (disagg) {
			SpatRaster tmp = out.to_memory_copy(xopt);
			std::vector<size_t> fact = {1,2};
			opt.overwrite=true;
			out = tmp.aggregate(fact, "sum", true, opt);
		}

	} else {
		if (transform) {
			transform = can_transform(source[0].srs.wkt, "EPSG:4326");
		}

		if (transform) {
			bool resample = false;
//			SpatRaster empty = out.geometry(1);
			size_t rcx = std::max(rcmax, 10);
			size_t frow = 1, fcol = 1;
			SpatRaster target = out.geometry(1);
			if ((nrow() > rcx) || (ncol() > rcx)) {
				resample = true;
				frow = (nrow() / rcx) + 1;
				fcol = (ncol() / rcx) + 1;
				out = out.aggregate({frow, fcol}, "mean", false, xopt);
				xopt.ncopies *= 5;
				if (!out.writeStart(xopt, filenames())) { return out; }
			} else {
				opt.ncopies *= 5;
				if (!out.writeStart(opt, filenames())) { return out; }
			}
			SpatRaster empty = out.geometry(1);
			SpatExtent extent = out.getExtent();
			double dy = out.yres() / 2;
			for (size_t i = 0; i < out.bs.n; i++) {
				double ymax = out.yFromRow(out.bs.row[i]) + dy;
				double ymin = out.yFromRow(out.bs.row[i] + out.bs.nrows[i]-1) - dy;
				SpatExtent e = {extent.xmin, extent.xmax, ymin, ymax};
				SpatRaster chunk = empty.crop(e, "near", false, xopt);
				SpatVector p = chunk.as_polygons(false, false, false, false, false, 0, xopt);
				std::vector<double> v = p.area(unit, true, {});
				if (!out.writeBlock(v, i)) return out;
				out.writeStop();
			}
			if (resample) {
				double divr = (double)(frow*fcol);
				out = out.arith(divr, "/", false, false, xopt);
				out = out.warper(target, "", "bilinear", false, false, true, opt);
			}
		} else {
			if (!out.writeStart(opt, filenames())) { return out; }
			double u = unit == "m" ? 1 : unit == "km" ? 1000000 : 10000;
			double m = out.source[0].srs.to_meter();
			double a = std::isnan(m) ? 1 : m;
			a *= xres() * yres() / u;
			for (size_t i = 0; i < out.bs.n; i++) {
				std::vector<double> v(out.bs.nrows[i]*ncol(), a);
				if (!out.writeBlock(v, i)) return out;
			}
			out.writeStop();
		}
	}

	if (mask) {
		out = out.mask(*this, false, NAN, NAN, mopt);
	}
	return(out);
}


std::vector<std::vector<double>> SpatRaster::sum_area(std::string unit, bool transform, bool by_value, SpatOptions &opt) {

	if (source[0].srs.wkt.empty()) {
		addWarning("unknown CRS. Results can be wrong");
		transform = false;
	}

	std::vector<std::string> f {"m", "km", "ha"};
	if (std::find(f.begin(), f.end(), unit) == f.end()) {
		setError("invalid unit");
		return {{NAN}};
	}

	if (transform) { //avoid very large polygon objects
		if (!is_lonlat()) {
			transform = can_transform(getSRS("wkt"), "+proj=longlat");
		}
	}
	BlockSize bs = getBlockSize(opt);
	if (!readStart()) {
		return {{NAN}};
	}
	size_t nc = ncol();
	size_t nl = nlyr();
	std::vector<double> out(nl, 0);
	std::vector<std::map<double, double>> m;
	if (by_value) {
		m.resize(nl);
	}

	if (is_lonlat()) {
		SpatRaster x = geometry(1);
		SpatExtent extent = x.getExtent();
		if ((nc == 1) && ((extent.xmax - extent.xmin) > 180)) {
			std::vector<size_t> fact= {1,2};
			x = x.disaggregate(fact, opt);
		}
		SpatExtent e = {extent.xmin, extent.xmin+x.xres(), extent.ymin, extent.ymax};
		SpatRaster onecol = x.crop(e, "near", false, opt);
		SpatVector p = onecol.as_polygons(false, false, false, false, false, 0, opt);
		std::vector<double> ar = p.area(unit, true, {});
		if (!hasValues()) {
			out.resize(1);
			for (size_t i=0; i<ar.size(); i++) {
				out[0] += ar[i] * (double)nc;
			}
		} else {
			for (size_t i=0; i<bs.n; i++) {
				std::vector<double> v;
				readValues(v, bs.row[i], bs.nrows[i], 0, ncol());
				size_t blockoff = bs.nrows[i] * nc;
				for (size_t lyr=0; lyr<nl; lyr++) {
					size_t lyroff = lyr * blockoff;
					if (by_value) {
						for (size_t j=0; j<bs.nrows[i]; j++) {
							size_t row = bs.row[i] + j;
							size_t offset = lyroff + j * nc;
							size_t n = offset + nc;
							for (size_t k=offset; k<n; k++) {
								if (!std::isnan(v[k])) {
									if (m[lyr].find(v[k]) == m[lyr].end()) {
										m[lyr][v[k]] = ar[row];
									} else {
										m[lyr][v[k]] += ar[row];
									}
								}
							}
						}
					} else {
						for (size_t j=0; j<bs.nrows[i]; j++) {
							size_t row = bs.row[i] + j;
							size_t offset = lyroff + j * nc;
							size_t n = offset + nc;
							for (size_t k=offset; k<n; k++) {
								if (!std::isnan(v[k])) out[lyr] += ar[row];
							}
						}
					}
				}
			}
		}
	} else if (transform) {
		opt.set_memfrac(std::max(0.1, opt.get_memfrac()/2));
		SpatRaster x = geometry(1);
		SpatExtent extent = x.getExtent();
		double dy = x.yres() / 2;
		SpatOptions popt(opt);
		if (!hasValues()) {
			out.resize(1);
			for (size_t i=0; i<bs.n; i++) {
				double ymax = x.yFromRow(bs.row[i]) + dy;
				double ymin = x.yFromRow(bs.row[i] + bs.nrows[i]-1) - dy;
				SpatExtent e = {extent.xmin, extent.xmax, ymin, ymax};
				SpatRaster onechunk = x.crop(e, "near", false, popt);
				SpatVector p = onechunk.as_polygons(false, false, false, false, false, 0, popt);
				p = p.project("EPSG:4326", false);
				std::vector<double> v = p.area(unit, true, 	{});
				out[0] += accumulate(v.begin(), v.end(), 0.0);
			}
		} else {
			for (size_t i=0; i<bs.n; i++) {
				double ymax = x.yFromRow(bs.row[i]) + dy;
				double ymin = x.yFromRow(bs.row[i] + bs.nrows[i]-1) - dy;
				SpatExtent e = {extent.xmin, extent.xmax, ymin, ymax};
				SpatRaster onechunk = x.crop(e, "near", false, popt);
				SpatVector p = onechunk.as_polygons(false, false, false, false, false, 0, popt);
				p = p.project("EPSG:4326", false);
				std::vector<double> ar = p.area(unit, true, {});
				std::vector<double> v;
				readValues(v, bs.row[i], bs.nrows[i], 0, ncol());
				size_t blockoff = bs.nrows[i] * nc;
				for (size_t lyr=0; lyr<nl; lyr++) {
					size_t lyroff = lyr * blockoff;
					if (by_value) {
						for (size_t j=0; j<bs.nrows[i]; j++) {
							size_t row = bs.row[i] + j;
							size_t offset = lyroff + j * nc;
							size_t n = offset + nc;
							for (size_t k=offset; k<n; k++) {
								if (!std::isnan(v[k])) {
									if (m[lyr].find(v[k]) == m[lyr].end()) {
										m[lyr][v[k]] = ar[row];
									} else {
										m[lyr][v[k]] += ar[row];
									}
								}
							}
						}
					} else {
						for (size_t j=0; j<bs.nrows[i]; j++) {
							size_t row = bs.row[i] + j;
							size_t offset = lyroff + j * nc;
							size_t n = offset + nc;
							for (size_t k=offset; k<n; k++) {
								if (!std::isnan(v[k])) out[lyr] += ar[row];
							}
						}
					}
				}
			}
		}
	} else {
		double adj = unit == "m" ? 1 : unit == "km" ? 1000000 : 10000;
		double unit = source[0].srs.to_meter();
		unit = std::isnan(unit) ? 1 : unit;
		double ar = xres() * yres() * unit * unit / adj;
		if (!hasValues()) {
			out.resize(1);
			out[0] = ncell() * ar;
		} else {
			for (size_t i=0; i<bs.n; i++) {
				std::vector<double> v;
				readValues(v, bs.row[i], bs.nrows[i], 0, ncol());
				size_t blockoff = bs.nrows[i] * nc;
				for (size_t lyr=0; lyr<nl; lyr++) {
					size_t offset = lyr * blockoff;
					size_t n = offset + blockoff;
					if (by_value) {
						for (size_t k=offset; k<n; k++) {
							if (!std::isnan(v[k])) {
								if (m[lyr].find(v[k]) == m[lyr].end()) {
									m[lyr][v[k]] = ar;
								} else {
									m[lyr][v[k]] += ar;
								}
							}
						}
					} else {
						for (size_t k=offset; k<n; k++) {
							if (!std::isnan(v[k])) {
								out[lyr] += ar;
							}
						}
					}
				}
			}
		}
	}
	readStop();
	if (by_value) {
		std::vector<std::vector<double>> dout(nl);
		for (size_t i=0; i<nl; i++) {
			for (auto& it:m[i]) {
				dout[i].push_back(it.first);
				dout[i].push_back(it.second);
			}
		}
		return dout;
	} 
	return {out};
}




std::vector<std::vector<double>> SpatRaster::sum_area_group(SpatRaster group, std::string unit, bool transform, bool by_value, SpatOptions &opt) {

	if (source[0].srs.wkt.empty()) {
		addWarning("unknown CRS. Results can be wrong");
		transform = false;
	}

	if (!(hasValues() && group.hasValues())) {
		setError("raster has no values");
		return {{NAN}};		
	}

	std::vector<std::string> f {"m", "km", "ha"};
	if (std::find(f.begin(), f.end(), unit) == f.end()) {
		setError("invalid unit");
		return {{NAN}};
	}

	BlockSize bs = getBlockSize(opt);
	if (!readStart()) {
		return {{NAN}};
	}
	if (!group.readStart()) {
		return {{NAN}};
	}

	size_t nc = ncol();
	size_t nl = nlyr();
	std::vector<std::map<double, std::map<double, double>>> m(nl);

	if (is_lonlat()) {
		SpatRaster x = geometry(1);
		SpatExtent extent = x.getExtent();
		if ((nc == 1) && ((extent.xmax - extent.xmin) > 180)) {
			std::vector<size_t> fact= {1,2};
			x = x.disaggregate(fact, opt);
		}
		SpatExtent e = {extent.xmin, extent.xmin+x.xres(), extent.ymin, extent.ymax};
		SpatRaster onecol = x.crop(e, "near", false, opt);
		SpatVector p = onecol.as_polygons(false, false, false, false, false, 0, opt);
		std::vector<double> ar = p.area(unit, true, {});
		for (size_t i=0; i<bs.n; i++) {
			std::vector<double> v, g;
			readValues(v, bs.row[i], bs.nrows[i], 0, ncol());
			group.readValues(g, bs.row[i], bs.nrows[i], 0, ncol());
			size_t blockoff = bs.nrows[i] * nc;
			for (size_t lyr=0; lyr<nl; lyr++) {
				size_t lyroff = lyr * blockoff;
				if (by_value) {
					for (size_t j=0; j<bs.nrows[i]; j++) {
						size_t row = bs.row[i] + j;
						size_t rowoff = j * nc;
						size_t offset = lyroff + rowoff;
						for (size_t k=0; k<nc; k++) {
							double vk = v[k+offset];
							double gk = g[k+rowoff];
							if (!(std::isnan(vk) || std::isnan(gk))) {
								if (m[lyr].find(vk) == m[lyr].end() || 
										m[lyr][vk].find(gk) == m[lyr][vk].end()) {	
									m[lyr][vk][gk] = ar[row];
								} else {
									m[lyr][vk][gk] += ar[row];
								}
							}
						}
					}
				} else {
					for (size_t j=0; j<bs.nrows[i]; j++) {
						size_t row = bs.row[i] + j;
						size_t rowoff = j * nc;
						size_t offset = lyroff + rowoff;
						for (size_t k=0; k<nc; k++) {
							double vk = v[k+offset];
							double gk = g[k+rowoff];
							if (!(std::isnan(vk) || std::isnan(gk))) {
								vk = 0;
								if (m[lyr].find(vk) == m[lyr].end() || 
										m[lyr][vk].find(gk) == m[lyr][vk].end()) {	
									m[lyr][vk][gk] = ar[row];
								} else {
									m[lyr][vk][gk] += ar[row];
								}
							}
						}
					}
				}
			}
		}
	} else {
		transform = transform && can_transform(getSRS("wkt"), "+proj=longlat");
		if (transform) {
			opt.set_memfrac(std::max(0.1, opt.get_memfrac()/2));
			SpatRaster x = geometry(1);
			SpatExtent extent = x.getExtent();
			double dy = x.yres() / 2;
			SpatOptions popt(opt);
			for (size_t i=0; i<bs.n; i++) {
				double ymax = x.yFromRow(bs.row[i]) + dy;
				double ymin = x.yFromRow(bs.row[i] + bs.nrows[i]-1) - dy;
				SpatExtent e = {extent.xmin, extent.xmax, ymin, ymax};
				SpatRaster onechunk = x.crop(e, "near", false, popt);
				SpatVector p = onechunk.as_polygons(false, false, false, false, false, 0, popt);
				p = p.project("EPSG:4326", false);
				std::vector<double> ar = p.area(unit, true, {});
				std::vector<double> v, g;
				readValues(v, bs.row[i], bs.nrows[i], 0, ncol());
				group.readValues(g, bs.row[i], bs.nrows[i], 0, ncol());
				size_t blockoff = bs.nrows[i] * nc;
				for (size_t lyr=0; lyr<nl; lyr++) {
					size_t lyroff = lyr * blockoff;
					if (by_value) {
						for (size_t j=0; j<bs.nrows[i]; j++) {
							size_t row = bs.row[i] + j;
							size_t rowoff = j * nc;
							size_t offset = lyroff + rowoff;
							for (size_t k=0; k<nc; k++) {
								double vk = v[k+offset];
								double gk = g[k+rowoff];							
								if (!(std::isnan(vk) || std::isnan(gk))) {
									if (m[lyr].find(vk) == m[lyr].end() ||
											m[lyr][vk].find(gk) == m[lyr][vk].end()) {	
										m[lyr][vk][gk] = ar[row];
									} else {
										m[lyr][vk][gk] += ar[row];
									}
								}
							}
						}
					} else {
						for (size_t j=0; j<bs.nrows[i]; j++) {
							size_t row = bs.row[i] + j;
							size_t rowoff = j * nc;
							size_t offset = lyroff + rowoff;
							for (size_t k=0; k<nc; k++) {
								double vk = v[k+offset];
								double gk = g[k+rowoff];							
								if (!(std::isnan(vk) || std::isnan(gk))) {
									vk = 0;
									if (m[lyr].find(vk) == m[lyr].end() ||
											m[lyr][vk].find(gk) == m[lyr][vk].end()) {	
										m[lyr][vk][gk] = ar[row];
									} else {
										m[lyr][vk][gk] += ar[row];
									}
								}
							}
						}
					}
				}
			}
		} else {
			double adj = unit == "m" ? 1 : unit == "km" ? 1000000 : 10000;
			double unit = source[0].srs.to_meter();
			unit = std::isnan(unit) ? 1 : unit;
			double ar = xres() * yres() * unit * unit / adj;
			for (size_t i=0; i<bs.n; i++) {
				std::vector<double> v, g;
				readValues(v, bs.row[i], bs.nrows[i], 0, ncol());
				group.readValues(g, bs.row[i], bs.nrows[i], 0, ncol());
				size_t blockoff = bs.nrows[i] * nc;
				for (size_t lyr=0; lyr<nl; lyr++) {
					size_t offset = lyr * blockoff;
					if (by_value) {
						for (size_t k=0; k<blockoff; k++) {
							double vk = v[offset+k];
							double gk = g[blockoff+k];
							if (!(std::isnan(vk) || std::isnan(gk))) {
								if (m[lyr].find(vk) == m[lyr].end() || 
										m[lyr][vk].find(gk) == m[lyr][vk].end()) {	
									m[lyr][vk][gk] = ar;
								} else {
									m[lyr][vk][gk] += ar;
								}
							}
						}
					} else {
						for (size_t k=0; k<blockoff; k++) {
							double vk = v[offset+k];
							double gk = g[blockoff+k];
							if (!(std::isnan(vk) || std::isnan(gk))) {
								vk = 0;
								if (m[lyr].find(vk) == m[lyr].end() ||
										m[lyr][vk].find(gk) == m[lyr][vk].end()) {	
									m[lyr][vk][gk] = ar;
								} else {
									m[lyr][vk][gk] += ar;
								}
							}
						}
					}
				}
			}
		}
	}

	readStop();
	group.readStop();
	std::vector<std::vector<double>> out(nl);
	for (size_t i=0; i<nl; i++) {
		for (auto& it1:m[i]) {
			std::map<double, double> gm = it1.second;
			for (auto& it2:gm) {
				out[i].push_back((double)i);
				out[i].push_back(it1.first);
				out[i].push_back(it2.first);
				out[i].push_back(it2.second);
			}
		}
	} 
	return out;
}




size_t get_k(const std::vector<double> &r, std::default_random_engine &generator, std::uniform_int_distribution<> &U) {
	double dmin = 0;
	size_t k = 0;
	for (size_t j=0; j<8; j++) {
		if (r[j] > dmin) {
			dmin = r[j];
			k = j + 1;
		} else if (r[j] == dmin) {
			if (U(generator)) {
				dmin = r[j];
				k = j + 1;
			}
		}
	}
	return k;
}


void do_flowdir(std::vector<double> &val, std::vector<double> &d, size_t nrow, size_t ncol, double dx, double dy, unsigned seed, bool before, bool after) {

	if (!before) {
	//	val.resize(val.size() + ncol, NAN);
		std::vector<double> rna(ncol, NAN);
		d.insert(d.begin(), rna.begin(), rna.end());
		nrow++;
	}
	if (!after) {
	//	val.resize(val.size() + ncol, NAN);
		d.resize(d.size()+ncol, NAN);
		nrow++;
	}

	std::vector<double> r(8);
	std::vector<double> p = {0, 1, 2, 4, 8, 16, 32, 64, 128}; // pow(2, j)
	//std::vector<double> p2 = {0, 1, 2, 3, 4, 5, 6, 7, 8}; 
	double dxy = sqrt(dx * dx + dy * dy);

	std::default_random_engine generator(seed);
	std::uniform_int_distribution<> U(0, 1);

	size_t nc1 = ncol - 1;
	for (size_t row=1; row<(nrow-1); row++) {
		//first col
		size_t i = row * ncol;
		if (std::isnan(d[i])) {
			val.push_back( NAN );
		} else {
			r[0] = (d[i] - d[i+1]) / dx;
			r[1] = (d[i] - d[i+1+ncol]) / dxy;
			r[2] = (d[i] - d[i+ncol]) / dy;
			r[3] = NAN;
			r[4] = NAN;
			r[5] = NAN;
			r[6] = (d[i] - d[i-ncol]) / dy;
			r[7] = (d[i] - d[i+1-ncol]) / dxy;
			size_t k = get_k(r, generator, U);
			val.push_back( p[k] );
		}
		for (size_t col=1; col<nc1; col++) {
			i = row * ncol + col;
			if (!std::isnan(d[i])) {
				r[0] = (d[i] - d[i+1]) / dx;
				r[1] = (d[i] - d[i+1+ncol]) / dxy;
				r[2] = (d[i] - d[i+ncol]) / dy;
				r[3] = (d[i] - d[i-1+ncol]) / dxy;
				r[4] = (d[i] - d[i-1]) / dx;
				r[5] = (d[i] - d[i-1-ncol]) / dxy;
				r[6] = (d[i] - d[i-ncol]) / dy;
				r[7] = (d[i] - d[i+1-ncol]) / dxy;
				size_t k = get_k(r, generator, U);
				val.push_back( p[k] );
			} else {
				val.push_back( NAN );
			}
		}	
		//last col
		i = row * ncol + nc1;
		if (!std::isnan(d[i])) {
			r[0] = NAN;
			r[1] = NAN;
			r[2] = (d[i] - d[i+ncol]) / dy;
			r[3] = (d[i] - d[i-1+ncol]) / dxy;
			r[4] = (d[i] - d[i-1]) / dx;
			r[5] = (d[i] - d[i-1-ncol]) / dxy;
			r[6] = (d[i] - d[i-ncol]) / dy;
			r[7] = NAN;
			size_t k = get_k(r, generator, U);
			val.push_back( p[k] );
		} else {
			val.push_back( NAN );
		}
	}
/*	
	if (!before) {
		if (!after) {
			val = std::vector<double>(val.begin()+ncol, val.end()-ncol);
		} else {
			val = std::vector<double>(val.begin()+ncol, val.end());
		}
	} else if (!after) {
		val = std::vector<double>(val.begin(), val.end()-ncol);
	}
*/
//	if (!after) {
//		val.resize(val.size() + ncol, NAN);
//	}

}


void do_TRI(std::vector<double> &val, std::vector<double> const &d, size_t nrow, size_t ncol, bool before, bool after) {
	if (!before) {
		val.resize(val.size() + ncol, NAN);
	}
	for (size_t row=1; row< (nrow-1); row++) {
		val.push_back(NAN);
		for (size_t col=1; col< (ncol-1); col++) {
			size_t i = row * ncol + col;
			val.push_back(
				(fabs(d[i-1-ncol]-d[i]) + fabs(d[i-1]-d[i]) + fabs(d[i-1+ncol]-d[i]) +  fabs(d[i-ncol]-d[i]) + fabs(d[i+ncol]-d[i]) +  fabs(d[i+1-ncol]-d[i]) + fabs(d[i+1]-d[i]) +  fabs(d[i+1+ncol]-d[i])) / 8
			);
		}
		val.push_back(NAN);
	}
	if (!after) {
		val.resize(val.size() + ncol, NAN);
	}
}

inline double pow2(double x) {
	return pow(x, 2);
}

void do_TRI_riley(std::vector<double> &val, std::vector<double> const &d, size_t nrow, size_t ncol, bool before, bool after) {
	if (!before) {
		val.resize(val.size() + ncol, NAN);
	}
	for (size_t row=1; row< (nrow-1); row++) {
		val.push_back(NAN);
		for (size_t col=1; col< (ncol-1); col++) {
			size_t i = row * ncol + col;
			val.push_back(
				sqrt(pow2(d[i-1-ncol]-d[i]) + pow2(d[i-1]-d[i]) + pow2(d[i-1+ncol]-d[i]) + 
				pow2(d[i-ncol]-d[i]) + pow2(d[i+ncol]-d[i]) + pow2(d[i+1-ncol]-d[i]) + 
				pow2(d[i+1]-d[i]) + pow2(d[i+1+ncol]-d[i]))
			);
		}
		val.push_back(NAN);
	}
	if (!after) {
		val.resize(val.size() + ncol, NAN);
	}
}

void do_TRI_rmsd(std::vector<double> &val, std::vector<double> const &d, size_t nrow, size_t ncol, bool before, bool after) {
	if (!before) {
		val.resize(val.size() + ncol, NAN);
	}
	for (size_t row=1; row< (nrow-1); row++) {
		val.push_back(NAN);
		for (size_t col=1; col< (ncol-1); col++) {
			size_t i = row * ncol + col;
			val.push_back(
				sqrt((pow2(d[i-1-ncol]-d[i]) + pow2(d[i-1]-d[i]) + pow2(d[i-1+ncol]-d[i]) + 
				pow2(d[i-ncol]-d[i]) + pow2(d[i+ncol]-d[i]) + pow2(d[i+1-ncol]-d[i]) + 
				pow2(d[i+1]-d[i]) + pow2(d[i+1+ncol]-d[i]))/8)
			);
		}
		val.push_back(NAN);
	}
	if (!after) {
		val.resize(val.size() + ncol, NAN);
	}
}


void do_TPI(std::vector<double> &val, const std::vector<double> &d, const size_t nrow, const size_t ncol, bool before, bool after) {
	if (!before) {
		val.resize(val.size() + ncol, NAN);
	}
	for (size_t row=1; row< (nrow-1); row++) {
		val.push_back(NAN);
		for (size_t col=1; col< (ncol-1); col++) {
			size_t i = row * ncol + col;
			val.push_back( d[i] - (d[i-1-ncol] + d[i-1] + d[i-1+ncol] + d[i-ncol]
			+ d[i+ncol] + d[i+1-ncol] + d[i+1] + d[i+1+ncol]) / 8 );
		}
		val.push_back(NAN);
	}
/*
	if (expand) {
		for (size_t i=1; i < (ncol-1); i++) {
			val[i+add] = d[i] - (d[i-1] + d[i-1+ncol] + d[i+ncol] + d[i+1] + d[i+1+ncol]) / 5;
			size_t j = i+(nrow-1) * ncol;
			val[j+add] = d[j] - (d[j-1-ncol] + d[j-1] + d[j-ncol] + d[j+1-ncol] + d[j+1]) / 5;
		}
		for (size_t row=1; row< (nrow-1); row++) {
			size_t i = row * ncol;
			val[i+add] = d[i] - (d[i-ncol] + d[i+ncol] + d[i+1-ncol] + d[i+1] + d[i+1+ncol]) / 5;
			i += ncol - 1;
			val[i+add] = d[i] - (d[i-ncol] + d[i] + d[i+ncol] + d[i-ncol]) / 5;
		}
		size_t i = 0;
		val[i+add] = d[i] - (d[i+ncol] + d[i+1] + d[i+1+ncol]) / 3;
		i = ncol-1;
		val[i+add] = d[i] - (d[i+ncol] + d[i-1] + d[i-1+ncol]) / 3;
		i = (nrow-1)*ncol;
		val[i+add] = d[i] - (d[i-ncol] + d[i+1] + d[i+1-ncol]) / 3;
		i = (nrow*ncol)-1;
		val[i+add] = d[i] - (d[i-ncol] + d[i-1] + d[i-1-ncol]) / 3;
	}
*/
	if (!after) {
		val.resize(val.size() + ncol, NAN);
	}


}



void do_roughness(std::vector<double> &val, const std::vector<double> &d, size_t nrow, size_t ncol, bool before, bool after) {

	if (!before) {
		val.resize(val.size() + ncol, NAN);
	}

	int incol = (int)ncol;
	int a[9] = { -1-incol, -1, -1+incol, -incol, 0, incol, 1-incol, 1, 1+incol };
	double min, max, v;
	for (size_t row=1; row< (nrow-1); row++) {
		val.push_back(NAN);
		for (size_t col=1; col< (ncol-1); col++) {
			size_t i = row * ncol + col;
			min = d[i + a[0]];
			max = d[i + a[0]];
			for (size_t j = 1; j < 9; j++) {
				v = d[i + a[j]];
				if (v > max) {
					max = v;
				} else if (v < min) {
					min = v;
				}
			}
			val.push_back(max - min);
		}
		val.push_back(NAN);
	}
	if (!after) {
		val.resize(val.size() + ncol, NAN);
	}
}


#ifndef M_PI
#define M_PI (3.14159265358979323846)
#endif




void to_degrees(std::vector<double>& x, size_t start) {
	double adj = 180 / M_PI;
	for (size_t i=start; i<x.size(); i++) {
		x[i] *= adj;
	}
}


void do_slope(std::vector<double> &val, const std::vector<double> &d, size_t ngb, size_t nrow, size_t ncol, double dx, double dy, bool geo, std::vector<double> &gy, bool degrees, bool before, bool after) {

	size_t start = val.size();
	if (!before) {
		val.resize(start + ncol, NAN);
	}

	std::vector<double> ddx;
	if (geo) {
		ddx.resize(nrow);
		for (size_t i=0; i<nrow; i++) {
			ddx[i] = distance_lonlat(-dx, gy[i], dx, gy[i]) / 2 ;
		}
	}

	if (ngb == 4) {
		if (geo) {
			double xwi[2] = {-1,1};
			double xw[2] = {0,0};
			double yw[2] = {-1,1};

			for (size_t i=0; i<2; i++) {
				yw[i] = yw[i] / (2 * dy);
			}

			for (size_t row=1; row< (nrow-1); row++) {
				for (size_t k=0; k<2; k++) {
					xw[k] = xwi[k] / (-2 * ddx[row]);
				}
				val.push_back(NAN); // first col
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;

					double zx = d[i-1] * xw[0] + d[i+1] * xw[1];
					double zy = d[i-ncol] * yw[0] + d[i+ncol] * yw[1];
					val.push_back( atan(sqrt( pow(zy, 2) + pow(zx, 2) )));
				}
				val.push_back(NAN); // last col
			}
		} else { // ngb == 8

			double xw[2] = {-1,1};
			double yw[2] = {-1,1};
			for (size_t i=0; i<2; i++) {
				xw[i] /= -2 * dx;
				yw[i] /=  2 * dy;
			}
			for (size_t row=1; row< (nrow-1); row++) {
				val.push_back(NAN);
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;
					double zx = d[i-1] * xw[0] + d[i+1] * xw[1];
					double zy = d[i-ncol] * yw[0] + d[i+ncol] * yw[1];
					val.push_back(  atan( sqrt( pow(zy, 2) + pow(zx, 2)  )));
				}
				val.push_back(NAN);
			}
		}
	} else {

		if (geo) {

			double xwi[6] = {-1,-2,-1,1,2,1};
			double xw[6] = {0,0,0,0,0,0};
			double yw[6] = {-1,1,-2,2,-1,1};

			for (size_t i=0; i<6; i++) {
				yw[i] = yw[i] / (8 * dy);
			}

			for (size_t row=1; row< (nrow-1); row++) {
				for (size_t k=0; k<6; k++) {
					xw[k] = xwi[k] / (8 * ddx[row]);
				}
				val.push_back(NAN);
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;
					double zx = d[i-1-ncol] * xw[0] + d[i-1] * xw[1] + d[i-1+ncol] * xw[2]
					   + d[i+1-ncol] * xw[3] + d[i+1] * xw[4] + d[i+1+ncol] * xw[5];

					double zy = d[i-1-ncol] * yw[0] + d[i-1+ncol] * yw[1] + d[i-ncol] * yw[2]
							+ d[i+ncol] * yw[3] + d[i+1-ncol] * yw[4] + d[i+1+ncol] * yw[5];
					val.push_back( atan(sqrt( pow(zy, 2) + pow(zx, 2))));
				}
				val.push_back(NAN);
			}
		} else {

			double xw[6] = {-1,-2,-1,1,2,1};
			double yw[6] = {-1,1,-2,2,-1,1};
			for (size_t i=0; i<6; i++) {
				xw[i] /= -8 * dx;
				yw[i] /= 8 * dy;
			}
			for (size_t row=1; row< (nrow-1); row++) {
				val.push_back(NAN);
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;
					double zx = d[i-1-ncol] * xw[0] + d[i-1] * xw[1] + d[i-1+ncol] * xw[2]
							+ d[i+1-ncol] * xw[3] + d[i+1] * xw[4] + d[i+1+ncol] * xw[5];
					double zy = d[i-1-ncol] * yw[0] + d[i-1+ncol] * yw[1] + d[i-ncol] * yw[2]
							+ d[i+ncol] * yw[3] + d[i+1-ncol] * yw[4] + d[i+1+ncol] * yw[5];
					val.push_back( atan(sqrt( pow(zy, 2) + pow(zx, 2) )));
				}
				val.push_back(NAN);
			}
		}
	}
	if (!after) {
		val.resize(val.size() + ncol, NAN);
	}

	if (degrees) {
		to_degrees(val, start);
	}
}


double dmod(double x, double n) {
	return(x - n * std::floor(x/n));
}



void do_aspect(std::vector<double> &val, const std::vector<double> &d, size_t ngb, size_t nrow, size_t ncol, double dx, double dy, bool geo, std::vector<double> &gy, bool degrees, bool before, bool after) {

	size_t start = val.size();
	if (!before) {
		val.resize(start + ncol, NAN);
	}
	std::vector<double> ddx;
	if (geo) {
		ddx.resize(nrow);
		for (size_t i=0; i<nrow; i++) {
			ddx[i] = distance_lonlat(-dx, gy[i], dx, gy[i]) / 2 ;
		}
	}
	double zy, zx;

	//double const pi2 = M_PI / 2;
	double const twoPI = 2 * M_PI;
	double const halfPI = M_PI / 2;

	if (ngb == 4) {
		if (geo) {
			double xwi[2] = {-1,1};
			double xw[2] = {0,0};
			double yw[2] = {-1,1};

			for (size_t i=0; i<2; i++) {
				yw[i] = yw[i] / (2 * dy);
			}

			for (size_t row=1; row < (nrow-1); row++) {
				val.push_back(NAN);
				for (size_t k=0; k<2; k++) {
					xw[k] = xwi[k] / (-2 * ddx[row]);
				}
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;
					zx = d[i-1] * xw[0] + d[i+1] * xw[1];
					zy = d[i-ncol] * yw[0] + d[i+ncol] * yw[1];
					zx = atan2(zy, zx);
					val.push_back( std::fmod( halfPI - zx, twoPI) );
				}
				val.push_back(NAN);
			}
		} else {

			double xw[2] = {-1,1};
			double yw[2] = {-1,1};
			for (size_t i=0; i<2; i++) {
				xw[i] = xw[i] / (-2 * dx);
				yw[i] = yw[i] / (2 * dy);
			}
			for (size_t row=1; row< (nrow-1); row++) {
				val.push_back(NAN);
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;
					zx = d[i-1] * xw[0] + d[i+1] * xw[1];
					zy = d[i-ncol] * yw[0] + d[i+ncol] * yw[1];
					zx = atan2(zy, zx);
					val.push_back( dmod( halfPI - zx, twoPI));
				}
				val.push_back(NAN);
			}
		}
	} else { //	(ngb == 8) {

		if (geo) {

			double xwi[6] = {-1,-2,-1,1,2,1};
			double xw[6] = {0,0,0,0,0,0};
			double yw[6] = {-1,1,-2,2,-1,1};

			for (size_t i=0; i<6; i++) {
				yw[i] /= (8 * dy);
			}

			for (size_t row=1; row < (nrow-1); row++) {
				val.push_back(NAN);
				for (size_t k=0; k<6; k++) {
					xw[k] = xwi[k] / (-8 * ddx[row]);
				}
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;

					zx = d[i-1-ncol] * xw[0] + d[i-1] * xw[1] + d[i-1+ncol] * xw[2]
							+ d[i+1-ncol] * xw[3] + d[i+1] * xw[4] + d[i+1+ncol] * xw[5];
					zy = d[i-1-ncol] * yw[0] + d[i-1+ncol] * yw[1] + d[i-ncol] * yw[2]
							+ d[i+ncol] * yw[3] + d[i+1-ncol] * yw[4] + d[i+1+ncol] * yw[5];

					zx = atan2(zy, zx);
					val.push_back( dmod( halfPI - zx, twoPI) );
				}
				val.push_back(NAN);
			}

		} else {

			double xw[6] = {-1,-2,-1,1,2,1};
			double yw[6] = {-1,1,-2,2,-1,1};
			for (size_t i=0; i<6; i++) {
				xw[i] /= -8 * dx;
				yw[i] /= 8 * dy;
			}
			for (size_t row=1; row< (nrow-1); row++) {
				val.push_back(NAN);
				for (size_t col=1; col< (ncol-1); col++) {
					size_t i = row * ncol + col;
					zx = d[i-1-ncol] * xw[0] + d[i-1] * xw[1] + d[i-1+ncol] * xw[2]
						+ d[i+1-ncol] * xw[3] + d[i+1] * xw[4] + d[i+1+ncol] * xw[5];
					zy = d[i-1-ncol] * yw[0] + d[i-1+ncol] * yw[1] + d[i-ncol] * yw[2]
							+ d[i+ncol] * yw[3] + d[i+1-ncol] * yw[4] + d[i+1+ncol] * yw[5];
					zx = atan2(zy, zx);
					val.push_back( dmod( halfPI - zx, twoPI) );
				}
				val.push_back(NAN);
			}
		}
	}
	if (!after) {
		val.resize(val.size() + ncol, NAN);
	}

	if (degrees) {
		to_degrees(val, start);
	}
}


SpatRaster SpatRaster::terrain(std::vector<std::string> v, unsigned neighbors, bool degrees, unsigned seed, SpatOptions &opt) {

//TPI, TRI, aspect, flowdir, slope, roughness
	//std::sort(v.begin(), v.end());
	//v.erase(std::unique(v.begin(), v.end()), v.end());

	SpatRaster out = geometry(v.size());
	out.setNames(v);

	if (nlyr() > 1) {
		out.setError("terrain needs a single layer object");
		return out;
	}

	bool aspslope = false;
	std::vector<std::string> f {"TPI", "TRI", "TRIriley", "TRIrmsd", "aspect", "flowdir", "slope", "roughness"};
	for (size_t i=0; i<v.size(); i++) {
		if (std::find(f.begin(), f.end(), v[i]) == f.end()) {
			out.setError("unknown terrain variable: " + v[i]);
			return(out);
		}
		if (v[i] == "slope" || v[i] == "aspect") {
			aspslope=true;
		}
	}
	if ((v.size() == 1) && (v[0] == "flowdir")) {
		out.setValueType(1);
	}

	if ((neighbors != 4) && (neighbors != 8)) {
		out.setError("neighbors should be 4 or 8");
		return out;
	}
	bool lonlat = is_lonlat();
	double yr = yres();

	if (!readStart()) {
		out.setError(getError());
		return(out);
	}

	opt.minrows = 3;
  	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}
	size_t nc = ncol();

	if (nrow() < 3 || nc < 3) {
		for (size_t i = 0; i < out.bs.n; i++) {
			std::vector<double> val(out.bs.nrows[i] * nc, NAN);
			if (!out.writeBlock(val, i)) return out;
		}
		return out;
	}


	std::vector<double> y;
	for (size_t i = 0; i < out.bs.n; i++) {
		std::vector<double> d;
		bool before= false;
		bool after = false;
		size_t rrow = out.bs.row[i];
		size_t rnrw = out.bs.nrows[i];
		if (i > 0) {
			rrow--;
			rnrw++;
			before=true;
		}
		if ((out.bs.row[i] + out.bs.nrows[i]) < nrow()) {
			rnrw++;
			after = true;
		}
		readValues(d, rrow, rnrw, 0, nc);
		if (lonlat && aspslope) {
			std::vector<int64_t> rows(rnrw);
			std::iota(rows.begin(), rows.end(), rrow);
			y = yFromRow(rows);
			yr = distance_lonlat(0, 0, 0, yres());
		}
		std::vector<double> val;
		val.reserve(out.bs.nrows[i] * ncol() * v.size());
		for (size_t j =0; j<v.size(); j++) {
			if (v[j] == "slope") {
				do_slope(val, d, neighbors, rnrw, nc, xres(), yr, lonlat, y, degrees, before, after);
			} else if (v[j] == "aspect") {
				do_aspect(val, d, neighbors, rnrw, nc, xres(), yr, lonlat, y, degrees, before, after);
			} else if (v[j] == "flowdir") {
				double dx = xres();
				double dy = yres();
				if (lonlat) {
					double yhalf = yFromRow((size_t) nrow()/2);
					dx = distance_lonlat(0, yhalf, dx, yhalf);
					dy = distance_lonlat(0, 0, 0, dy);
				}
				do_flowdir(val, d, rnrw, nc, dx, dy, seed, before, after);
			} else if (v[j] == "roughness") {
				do_roughness(val, d, rnrw, nc, before, after);
			} else if (v[j] == "TPI") {
				do_TPI(val, d, rnrw, nc, before, after);
			} else if (v[j] == "TRI") {
				do_TRI(val, d, rnrw, nc, before, after);
			} else if (v[j] == "TRIriley") {
				do_TRI_riley(val, d, rnrw, nc, before, after);
			} else if (v[j] == "TRIrmsd") {
				do_TRI_rmsd(val, d, rnrw, nc, before, after);			
			} else {
				out.setError("?"); return out;
			}
		}
		if (!out.writeBlock(val, i)) return out;
	}
	out.writeStop();
	readStop();
	return out;
}




SpatRaster SpatRaster::hillshade(SpatRaster aspect, std::vector<double> angle, std::vector<double> direction, bool normalize, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if ((nlyr() != 1) || (aspect.nlyr() != 1)) {
		out.setError("slope and aspect should have one layer");
		return out;
	}
	if (angle.empty() || direction.empty()) {
		out.setError("you must provide a value for aspect and direction");
		return out;		
	}

	std::vector<std::string> nms;

	if ((angle.size() > 1) || (direction.size() > 1)) {
		recycle(angle, direction);
		recycle(direction, angle);	
		//nms = opt.names;
		SpatOptions ops(opt);
		ops.ncopies *= angle.size();
		size_t nl = angle.size();
		out.source.resize(nl);
		if (ops.names.size() == nl) {
			nms = opt.names;
		} else {
			nms.reserve(nl);
			for (size_t i=0; i<nl; i++) {
				std::string nmi = "hs_" + double_to_string(angle[i]) + "_" + double_to_string(direction[i]);
				nms.push_back(nmi);
			}
		}

		for (size_t i=0; i<nl; i++) {
			ops.names = {nms[i]};
			SpatRaster r = hillshade(aspect, {angle[i]}, {direction[i]}, normalize, ops);
			out.source[i] = r.source[0];
		}
		if (!opt.get_filename().empty()) {
			out = out.writeRaster(opt);
		}
		return out;
	} else {
		if ((opt.names.size() == 1) && !opt.names[0].empty()) {
			out.setNames(  {opt.names[0] } );
		} else {
			out.setNames( {"hillshade"} );
		}
	}

	double dir = direction[0] * M_PI / 180.0;
	double zen = (90.0 - angle[0]) * M_PI/180.0;
	double coszen = cos(zen);
	double sinzen = sin(zen);

	if (!readStart()) {
		out.setError(getError());
		return(out);
	}
	if (!aspect.readStart()) {
		out.setError(getError());
		return(out);
	}

  	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}

	for (size_t i = 0; i < out.bs.n; i++) {
		std::vector<double> slp;
		std::vector<double> asp;
		readBlock(slp, out.bs, i);
		aspect.readBlock(asp, out.bs, i);
		if (normalize) {
			for (size_t i=0; i<slp.size(); i++) {
				slp[i] = cos(slp[i]) * coszen + sin(slp[i]) * sinzen * cos(dir-asp[i]);
				if (slp[i] < 0) {
					slp[i] = 0;
				} else {
					slp[i] *= 255;
				}
			}
		} else {
			for (size_t i=0; i<slp.size(); i++) {
				slp[i] = cos(slp[i]) * coszen + sin(slp[i]) * sinzen * cos(dir-asp[i]);
			}
		}
		if (!out.writeBlock(slp, i)) return out;
	}
	out.writeStop();
	readStop();
	aspect.readStop();
	return out;
}


//=== surfaceArea ======================================

/* Compute surface area, using method from:
Jeff S. Jenness, 2004. Calculating Landscape Surface Area from Digital Elevation Models. Wildlife Society Bulletin 32(3):829-839. http://www.jstor.org/stable/3784807
With edge adjustments. 
From C code in R package "sp" by Barry Rowlingson <b.rowlingson@lancaster.ac.uk> (copyright 2010)
Adapted for terra by Robert Hijmans (C++, support for lonlat) 
*/

inline double height(const std::vector<double> &heights, const long &ncols, const size_t &row, const long &col) {
	size_t ncr = ncols*row;
	if (col < 0) {
		return heights[ncr]; // + 0
	} else if (col == ncols) {
		return heights[ncr + (ncols-1)];
	} else {
		return heights[ncr + col];
	}
}

inline double triarea(const double &a, const double &b, const double &c) {
	// triangle area given side lengths
	double s = (a + b + c) / 2.0;
	return sqrt(s * (s-a) * (s-b) * (s-c));
}


void sarea(std::vector<double> &heights, const size_t &nrow, const long &ncol, const std::vector<double> &w, const double &h, bool lonlat, std::vector<double> &sa) {

// given an nx by ny matrix of heights with single-cell edge border, compute the surface area.

// offsets to neighbours
	std::vector<int> dyv = {-1, -1, -1, 0, 1, 1, 1, 0, -1};
	std::vector<int> dxv = {-1, 0, 1, 1, 1, 0, -1, -1, -1};

// triangle diagonal length
	double s2 = sqrt((w[0]*w[0]) + (h*h));
// radial side lengths
	std::vector<double> side = {s2, h, s2, w[0], s2, h, s2, w[0], s2};
// outer edges lengths
	std::vector<double> l3v = {w[0], w[0], h, h, w[0], w[0], h, h};

	sa = std::vector<double>(heights.size() - 2*ncol, NAN);

	size_t cell = 0;
	for (size_t i=1; i<(nrow-1); i++){
		if (lonlat) {
			size_t k = i - 1;
			s2 = sqrt((w[k]*w[k]) + (h*h));
			side = {s2, h, s2, w[k], s2, h, s2, w[k], s2};
			l3v = {w[k], w[k], h, h, w[k], w[k], h, h};
		}

		for (long j=0; j<ncol; j++) {
			double z1 = height(heights, ncol, i, j);
			if (!std::isnan(z1)) {
				sa[cell] = 0;
				for (size_t tri=0; tri<8; tri++){
					double z2 = height(heights, ncol, i+dxv[tri], j+dyv[tri]);
					// replace missing adjacent values with the current cell value
					if (std::isnan(z2)) z2 = z1;
					double z3 = height(heights, ncol, i+dxv[tri+1], j+dyv[tri+1]);
					if (std::isnan(z3)) z3 = z1;
					double l1 = 0.5 * sqrt(side[tri] * side[tri] + (z1-z2) * (z1-z2));
					double l2 = 0.5 * sqrt(side[tri+1] * side[tri+1] + (z1-z3) * (z1-z3));
					double l3 = 0.5 * sqrt(l3v[tri] * l3v[tri] + (z2-z3) * (z2-z3));
					sa[cell] += triarea(l1, l2, l3);
				}
			}
			cell++;
		}	
	}
}


SpatRaster SpatRaster::surfaceArea(SpatOptions &opt) {

	SpatRaster out = geometry(1, false);
	bool lonlat = is_lonlat();
	if (lonlat) {
		out.setError("not implemented for lonlat rasters");
		return out;		
	}
	if (!hasValues()) {
		out.setError("cannot compute surfaceArea for a raster with no values");
		return out;
	}
	if (nlyr() != 1) {
		out.setError("can only compute surfaceArea for a single raster layer");
		return out;		
	}

	if (!readStart()) {
		out.setError(getError());
		return(out);
	}

  	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}

	BlockSize cbs = out.bs;
	for (size_t i = 0; i < (cbs.n-1); i++) {
		cbs.nrows[i] += 1;
	}	
	for (size_t i = 1; i < cbs.n; i++) {
		cbs.row[i] -= 1;
		cbs.nrows[i] += 1;
	}

	size_t nc = ncol();	
	double xr = xres();	
	std::vector<double> resx = { xr };	
	double resy = yres();
	if (lonlat) {
		resy = distance_lonlat(0, 0, 0, resy);		
	} 
	std::vector<int64_t> rows;

	for (size_t i = 0; i < cbs.n; i++) {
		std::vector<double> v;
		readBlock(v, cbs, i);
		if (i==0) {
			v.insert(v.begin(), v.begin(), v.begin()+nc);
		}
		if (i == (cbs.n - 1)) {
			v.insert(v.end(), v.end()-nc, v.end());
		}
		if (lonlat) {
			rows.resize(out.bs.nrows[i]);
			std::iota(rows.begin(), rows.end(), out.bs.row[i]);
			std::vector<double> y = yFromRow(rows);
			resx = distance_lon(xr, y);
		}
		std::vector<double> sa;
		sarea(v, out.bs.nrows[i]+2, nc, resx, resy, lonlat, sa);
		if (!out.writeBlock(sa, i)) return out;
	}
	readStop();
	out.writeStop();
	return(out);
}


//=== patches ======================================


struct PatchUnionFind {
	std::vector<double> parents;
	size_t start_id;

	PatchUnionFind(size_t start) : start_id(start) {}

	void make_set(double id) {
		parents.push_back(id); 
	}

	double find(double id) {
		if (std::isnan(id)) return id;
		if (id < start_id) return id; 

		size_t idx = (size_t)(id - start_id);
		if (parents[idx] == id) return id;

		parents[idx] = find(parents[idx]); 
		return parents[idx];
	}

	void unite(double id1, double id2, std::vector<std::vector<size_t>>& rcl) {
		double root1 = find(id1);
		double root2 = find(id2);
		if (root1 == root2) return;

		bool g1 = root1 < start_id;
		bool g2 = root2 < start_id;

		if (g1 && g2) {
			rcl[0].push_back((size_t)root1);
			rcl[1].push_back((size_t)root2);
			return;
		}

		if (g1) {
			parents[(size_t)(root2 - start_id)] = root1;
		} else if (g2) {
			parents[(size_t)(root1 - start_id)] = root2;
		} else {
			if (root1 < root2)
				parents[(size_t)(root2 - start_id)] = root1;
			else
				parents[(size_t)(root1 - start_id)] = root2;
		}
	}
};

void broom_patches(const std::vector<double> &vals, std::vector<double> &patches, std::vector<double>& above_p, std::vector<double>& above_v, const size_t &dirs, size_t &ncps, const size_t &nr, const size_t &nc, std::vector<std::vector<size_t>> &rcl, bool is_global) {

	PatchUnionFind uf(ncps);
	bool d4 = dirs == 4;
	size_t stopnc = nc-1;
	std::vector<double> d;

	auto check_neighbor = [&](double val, double neighbor_val, double neighbor_patch, std::vector<double>& candidates) {
		if (!std::isnan(neighbor_val) && !std::isnan(neighbor_patch)) {
			if (is_equal(val, neighbor_val)) {
				candidates.push_back(neighbor_patch);
			}
		}
	};

	auto assign_patch = [&](size_t idx, std::vector<double>& candidates) {
		if (candidates.empty()) {
			patches[idx] = ncps;
			uf.make_set(ncps);
			ncps++;
		} else {
			double chosen = candidates[0];
			patches[idx] = chosen;
			for (size_t k = 1; k < candidates.size(); ++k) {
				uf.unite(chosen, candidates[k], rcl);
			}
		}
	};

	//first row
	if ( !std::isnan(vals[0]) ) {
		d.clear();
		if (d4) {
			check_neighbor(vals[0], above_v[0], above_p[0], d);
		} else if (is_global) { 
			check_neighbor(vals[0], above_v[0], above_p[0], d);
			check_neighbor(vals[0], above_v[1], above_p[1], d);
			check_neighbor(vals[0], above_v[stopnc], above_p[stopnc], d);
		} else { 
			check_neighbor(vals[0], above_v[0], above_p[0], d);
			check_neighbor(vals[0], above_v[1], above_p[1], d);
		}
		assign_patch(0, d);
	}

	for (size_t i=1; i<stopnc; i++) {
		if (!std::isnan(vals[i])) {
			d.clear();
			check_neighbor(vals[i], above_v[i], above_p[i], d);
			check_neighbor(vals[i], vals[i-1], patches[i-1], d);
			if (!d4) {
				check_neighbor(vals[i], above_v[i-1], above_p[i-1], d);
				check_neighbor(vals[i], above_v[i+1], above_p[i+1], d);
			}
			assign_patch(i, d);
		}
	}

	size_t i = stopnc;
	if (!std::isnan(vals[i])) {
		d.clear();
		check_neighbor(vals[i], above_v[i], above_p[i], d);
		check_neighbor(vals[i], vals[i-1], patches[i-1], d);
		if (is_global) {
			check_neighbor(vals[i], vals[0], patches[0], d);
			if (!d4) {
				check_neighbor(vals[i], above_v[i-1], above_p[i-1], d);
				check_neighbor(vals[i], above_v[0], above_p[0], d);
			}
		} else if (!d4) {
			check_neighbor(vals[i], above_v[i-1], above_p[i-1], d);
		}
		assign_patch(i, d);
	}

	for (size_t r=1; r<nr; r++) {
		size_t start = r*nc;
		size_t i=start;
		if (!std::isnan(vals[i])) {
			d.clear();
			check_neighbor(vals[i], vals[i-nc], patches[i-nc], d);
			if (is_global) {
				if (!d4) {
					check_neighbor(vals[i], vals[i-nc+1], patches[i-nc+1], d);
					check_neighbor(vals[i], vals[i-1], patches[i-1], d);
				}
			} else if (!d4) {
				check_neighbor(vals[i], vals[i-nc+1], patches[i-nc+1], d);
			}
			assign_patch(i, d);
		}

		size_t stop = start + stopnc;
		for (size_t i=(start+1); i<stop; i++) {
			if (!std::isnan(vals[i])) {
				d.clear();
				check_neighbor(vals[i], vals[i-1], patches[i-1], d);
				check_neighbor(vals[i], vals[i-nc], patches[i-nc], d);
				if (!d4) {
					check_neighbor(vals[i], vals[i-nc-1], patches[i-nc-1], d);
					check_neighbor(vals[i], vals[i-nc+1], patches[i-nc+1], d);
				}
				assign_patch(i, d);
			}
		}

		i = stop;
		if (!std::isnan(vals[i])) {
			d.clear();
			check_neighbor(vals[i], vals[i-1], patches[i-1], d);
			check_neighbor(vals[i], vals[i-nc], patches[i-nc], d);
			if (is_global) {
				check_neighbor(vals[i], vals[start], patches[start], d);
				if (!d4) {
					check_neighbor(vals[i], vals[i-nc-1], patches[i-nc-1], d);
					check_neighbor(vals[i], vals[start-nc], patches[start-nc], d);
				}
			} else if (!d4) {
				check_neighbor(vals[i], vals[i-nc-1], patches[i-nc-1], d);
			}
			assign_patch(i, d);
		}
	}

	for (size_t k = 0; k < patches.size(); ++k) {
		if (!std::isnan(patches[k])) {
			patches[k] = uf.find(patches[k]);
		}
	}

	size_t off = (nr-1) * nc;
	std::vector<double> last_row_p(patches.begin()+off, patches.end());
	std::vector<double> last_row_v(vals.begin()+off, vals.end());
	above_p = last_row_p;
	above_v = last_row_v;
}

std::vector<std::vector<double>> patches_getRCL(std::vector<std::vector<size_t>> rcl, size_t n) {
	std::vector<std::vector<size_t>> rcl2(rcl[0].size());
	for (size_t i=0; i<rcl[0].size(); i++) {
		rcl2[i].push_back(rcl[0][i]);
		rcl2[i].push_back(rcl[1][i]);
	}
    std::sort(rcl2.begin(), rcl2.end());
    rcl2.erase(std::unique(rcl2.begin(), rcl2.end()), rcl2.end());
	std::vector<std::vector<double>> out(2);
	for (size_t i=0; i<rcl2.size(); i++) {
		out[0].push_back(rcl2[i][1]);
		out[1].push_back(rcl2[i][0]);
	}

	for (size_t i=1; i<out[0].size(); i++) {
		for (size_t j=0; j<i; j++) {
			if (out[0][i] == out[1][j]) {
				out[1][j] = out[0][i];
			}
		}
	}

	std::vector<double> lost = out[0];
	lost.push_back(n);
	size_t sub = 0;
	for (size_t i=0; i<lost.size(); i++) {
		sub++;
		for (size_t j=lost[i]+1; j<lost[i+1]; j++) {
			out[0].push_back(j);
			out[1].push_back(j-sub);
		}
	}
	return out;
}

SpatRaster SpatRaster::patches(size_t directions, SpatOptions &opt) {

	SpatRaster out = geometry(1, false);

	if (nlyr() > 1) {
		SpatOptions ops(opt);
		std::vector<std::string> nms = getNames();
		if (ops.names.size() == nms.size()) {
			nms = opt.names;
		}
		for (size_t i=0; i<nlyr(); i++) {
			std::vector<size_t> lyr = {i};
			ops.names = {nms[i]};
			SpatRaster x = subset(lyr, ops);
			x = x.patches(directions, ops); 
			out.addSource(x, false, ops);
		}
		if (!opt.get_filename().empty()) {
			out = out.writeRaster(opt);
		}
		return out;
	}

	if (!hasValues()) {
		out.setError("cannot compute patches for a raster with no values");
		return out;
	}
	if (!((directions == 4) || (directions == 8))) {
		out.setError("directions should be 4 or 8");
		return out;		
	}

	if (!readStart()) {
		out.setError(getError());
		return(out);
	}

	opt.set_filenames({""}); 
  	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}

    size_t nc = ncol();
	size_t ncps = 1;
	std::vector<double> above_p(nc, NAN);
	std::vector<double> above_v(nc, NAN);
	std::vector<std::vector<size_t>> rcl(2);
	std::vector<double> v;

	bool is_global = is_global_lonlat();

	for (size_t i = 0; i < out.bs.n; i++) {
		readBlock(v, out.bs, i);
		std::vector<double> p(v.size(), NAN); 
		broom_patches(v, p, above_p, above_v, directions, ncps, out.bs.nrows[i], nc, rcl, is_global);
		if (!out.writeBlock(p, i)) return out;
	}

	readStop();
	out.writeStop();

	std::string filename = opt.get_filename();
	opt.set_filenames({filename});

	if (!rcl[0].empty()) {
		std::vector<std::vector<double>> rc = patches_getRCL(rcl, ncps);
		out = out.reclassify(rc, 3, true, false, 0.0, false, false, false, opt);
	} else if (!filename.empty()) {
		out = out.writeRaster(opt);
	}

	return(out);
}

//=== patches (end) ==================================


//=== nearest ========================================

std::vector<double> dn_bounds(const std::vector<double>& vx, const std::vector<double>& vy, const std::vector<double>& rx, const double& ry, size_t& first, size_t& last, const bool& lonlat, const std::string &method) {

	std::vector<double> d(rx.size(), std::numeric_limits<double>::max());
	size_t oldfirst = first;
	first = vx.size();
	last = 0;


	if (lonlat) {
		std::function<double(double, double, double, double)> dfun;
		if (method == "haversine") {
			dfun = distance_hav;
		} else if (method == "cosine") {
			dfun = distance_cos;			
		} else {
			dfun = distance_geo;
		}


		for (size_t i=0; i<rx.size(); i++) {
			size_t thisone = 0;
			for (size_t j=oldfirst; j<vx.size(); j++) {
				double dd = dfun(rx[i], ry, vx[j], vy[j]);
				if (dd < d[i]) {
					d[i] = dd;
					thisone = j;
				}
			}
			first = std::min(thisone, first);
			last  = std::max(thisone, last);
		}
	} else {
		for (size_t i=0; i<rx.size(); i++) {
			size_t thisone = 0;
			for (size_t j=oldfirst; j<vx.size(); j++) {
				double dd = distance_plane(rx[i], ry, vx[j], vy[j]);
				if (dd < d[i]) {
					d[i] = dd;
					thisone = j;
				}
			}
			first = std::min(thisone, first);
			last  = std::max(thisone, last);
		}
	}
	last += 1;
	return d;
}


void dn_only(std::vector<double> &d, const std::vector<double>& vx, const std::vector<double>& vy, const std::vector<double>& rx, const std::vector<double>& ry, const size_t& first, const size_t& last, const bool& lonlat, const std::vector<double>& dlast, bool skip, const std::string& method, bool setNA) {


std::vector<double> clast;

std::vector<double> cell;
const std::vector<double> v;

	size_t rxs = rx.size();
	d.reserve(rxs + dlast.size());
	cell.reserve(rxs + dlast.size());

	double inf = std::numeric_limits<double>::infinity();

	if (lonlat) {

		if (method == "geo") {

			double dd, azi1, azi2;
			struct geod_geodesic g;
			// get a and f from crs?
			double a = 6378137.0;
			double f = 1/298.257223563;
			geod_init(&g, a, f);

			if (skip) {
				for (size_t i=0; i<rxs; i++) {
					if (std::isnan(v[i])) {
						d.push_back(inf);
						cell.push_back(NAN);
						for (size_t j=first; j<last; j++) {
							geod_inverse(&g, ry[i], rx[i], vy[j], vx[j], &dd, &azi1, &azi2);
							if (dd < d[i]) {
								d[i] = dd;
								cell[i] = j;
							}
						}
					} else {
						d.push_back(0);
						cell.push_back(NAN);
					}
				}
			} else { // no skip

				for (size_t i=0; i<rxs; i++) {
					d.push_back(inf);
					cell.push_back(NAN);
					for (size_t j=first; j<last; j++) {
						geod_inverse(&g, ry[i], rx[i], vy[j], vx[j], &dd, &azi1, &azi2);
						if (dd < d[i]) {
							d[i] = dd;
							cell[i] = j;
						}
					}
				} 
			}

		} else { // not geo

			std::function<double(double, double, double, double)> dfun;
			if (method == "haversine") {
				dfun = distance_hav;
			} else if (method == "cosine") {
				dfun = distance_cos;
			} 
			if (skip) {

				for (size_t i=0; i<rxs; i++) {
					if (std::isnan(v[i])) {
						d.push_back(inf);
						cell.push_back(NAN);
						for (size_t j=first; j<last; j++) {
							double dd = dfun(rx[i], ry[i], vx[j], vy[j]);
							if (dd < d[i]) {
								d[i] = dd;
								cell[i] = j;
							}
						}
					} else {
						d.push_back(0);
						cell.push_back(NAN);
					}
				}		
			} else {	
				for (size_t i=0; i<rxs; i++) {
					d.push_back(inf);
					cell.push_back(NAN);
					for (size_t j=first; j<last; j++) {
						//double dd = distHaversine(rx[i], ry[i], vx[j], vy[j]);
						double dd = dfun(rx[i], ry[i], vx[j], vy[j]);

						if (dd < d[i]) {
							d[i] = dd;
							cell[i] = j;
						}
					}
				}
			} 
		}

	} else { // not lonlat
		if (skip) {
			for (size_t i=0; i<rxs; i++) {
				if (std::isnan(v[i])) {
					d.push_back(inf);
					cell.push_back(NAN);
					for (size_t j=first; j<last; j++) {
						double dd = distance_plane(rx[i], ry[i], vx[j], vy[j]);
						if (dd < d[i]) {
							d[i] = dd;
							cell[i] = j;
						}
					}
				} else {
					d.push_back(0);
					cell.push_back(NAN);
				}
			}
		} else {
			for (size_t i=0; i<rxs; i++) {
				d.push_back(inf);
				cell.push_back(NAN);
				for (size_t j=first; j<last; j++) {
					double dd = distance_plane(rx[i], ry[i], vx[j], vy[j]);
					if (dd < d[i]) {
						d[i] = dd;
						cell[i] = j;
					}
				}
			}
		}
	}

	d.insert(d.end(), dlast.begin(), dlast.end());
	cell.insert(cell.end(), clast.begin(), clast.end());
	if (skip) {
		for (size_t i=rxs; i< v.size(); i++) {
			if (!std::isnan(v[i])) {
				d[i] = 0;
				cell[i] = NAN;
			}
		}
		if (setNA) {
			double mxval = std::numeric_limits<double>::max();
			for (size_t i=0; i< v.size(); i++) {
				if (v[i] == mxval) {
					d[i] = NAN;
					cell[i] = NAN;
				}
			}
		}
	}
}


SpatRaster SpatRaster::dn_crds(std::vector<double>& x, std::vector<double>& y, const std::string& method, bool skip, bool setNA, std::string unit, SpatOptions &opt) {

	SpatRaster out = geometry();
	if (x.empty()) {
		out.setError("no locations to compute distance from");
		return(out);
	}
	const double toRad = 0.0174532925199433;
	std::vector<std::size_t> pm = sort_order_d(y);
	permute(x, pm);
	permute(y, pm);

	bool lonlat = is_lonlat(); 

	double m=1;
	if (!source[0].srs.m_dist(m, lonlat, unit)) {
		out.setError("invalid unit");
		return(out);
	}


	unsigned nc = ncol();
	opt.steps = std::max(opt.steps, (size_t) 4);
	opt.progress = opt.progress * 1.5;

 	if (!out.writeStart(opt, filenames())) {
		readStop();
		return out;
	}
	std::vector<double> cells;
	std::vector<double> dlast;

	std::vector<int64_t> cols;
	cols.resize(ncol());
	std::iota(cols.begin(), cols.end(), 0);
	std::vector<double> tox = xFromCol(cols);

	if (lonlat && (method != "geo")) {
		for (double &d : x) d *= toRad;
		for (double &d : y) d *= toRad;
		for (double &d : tox) d *= toRad;
	}

	double oldfirst = 0;
	size_t first = 0;
	size_t last  = x.size();

	std::vector<double> v;
	if (skip) {
		if (!readStart()) {
			out.setError(getError());
			return(out);
		}
		for (size_t i = 0; i < out.bs.n; i++) {
			cells.resize((out.bs.nrows[i] -1) * nc) ;
			std::iota(cells.begin(), cells.end(), out.bs.row[i] * nc);
			std::vector<std::vector<double>> rxy = xyFromCell(cells);
			double toy = yFromRow(out.bs.row[i] + out.bs.nrows[i] - 1);
			if (lonlat && (method != "geo")) {
				toy *= toRad;
				for (double &d : rxy[0]) d *= toRad;
				for (double &d : rxy[1]) d *= toRad;
			}
			readBlock(v, out.bs, i);
			dlast = dn_bounds(x, y, tox, toy, first, last, lonlat, method);
			std::vector<double> d;

			dn_only(d, x, y, rxy[0], rxy[1], oldfirst, last, lonlat, dlast, true, method, setNA);
			oldfirst = first;
			if (m != 1) {
				for (double &v : d) v *= m;
			}
			if (!out.writeBlock(d, i)) return out;
		}
		readStop();
	} else {
		for (size_t i = 0; i < out.bs.n; i++) {
			double toy = yFromRow(out.bs.row[i] + out.bs.nrows[i] - 1);
			cells.resize((out.bs.nrows[i] -1) * nc) ;
			std::iota(cells.begin(), cells.end(), out.bs.row[i] * nc);
			std::vector<std::vector<double>> rxy = xyFromCell(cells);
			if (lonlat && (method != "geo")) {
				toy *= toRad;
				for (double &d : rxy[0]) d *= toRad;
				for (double &d : rxy[1]) d *= toRad;
			}
			dlast = dn_bounds(x, y, tox, toy, first, last, lonlat, method);
			std::vector<double> d;
			dn_only(d, x, y, rxy[0], rxy[1], oldfirst, last, lonlat, dlast, false, method, setNA);
			oldfirst = first;
			if (m != 1) {
				for (double &v : d) v *= m;
			}
			if (!out.writeBlock(d, i)) return out;
		}
	}
	out.writeStop();
	return(out);
}




SpatRaster SpatRaster::nearest(double target, double exclude, bool keepNA, std::string unit, bool remove_zero, std::string method, SpatOptions &opt) {

	SpatRaster out = geometry(1);
	if (!hasValues()) {
		out.setError("SpatRaster has no values");
		return out;
	}

	SpatOptions ops(opt);
	size_t nl = nlyr();
	if (nl > 1) {
		std::vector<std::string> nms = getNames();
		if (ops.names.size() == nms.size()) {
			nms = opt.names;
		}
		out.source.resize(nl);
		for (size_t i=0; i<nl; i++) {
			std::vector<size_t> lyr = {i};
			SpatRaster r = subset(lyr, ops);
			ops.names = {nms[i]};
			r = r.nearest(target, exclude, keepNA, unit, remove_zero, method, ops);
			out.source[i] = r.source[0];
		}
		if (!opt.get_filename().empty()) {
			out = out.writeRaster(opt);
		}
		return out;
	}

	bool setNA = false;
	std::vector<std::vector<double>> p;
	if (!std::isnan(exclude)) {
		SpatRaster x;
		if (std::isnan(target)) {
			x = replaceValues({exclude}, {target}, 1, false, NAN, false, ops);
			x = x.edges(false, false, "inner", 8, 1, ops);
			p = x.as_points_value(1, ops);
			if (p.empty()) {
				return out.init({0}, opt);
			}
			return dn_crds(p[0], p[1], method, true, setNA, unit, opt);

		} else {
			x = replaceValues({exclude, target}, {NAN, NAN}, 1, false, NAN, false, ops);
			x = x.edges(false, false, "inner", 8, 1, ops);
			p = x.as_points_value(1, ops);
			out = replaceValues({NAN, exclude, target}, {target, NAN, NAN}, 1, false, NAN, false, ops);
		}
	} else if (!std::isnan(target)) {
		SpatRaster x = replaceValues({target}, {NAN}, 1, false, NAN, false, ops);
		x = x.edges(false, false, "inner", 8, 0, ops);
		p = x.as_points_value(1, ops);
		out = replaceValues({NAN, target}, {std::numeric_limits<double>::max(), NAN}, 1, false, NAN, false, ops);
		setNA = true;
	} else {
		out = edges(false, false, "inner", 8, 0, ops);
		p = out.as_points_value(1, ops);
	}
	if (p.empty()) {
		return out.init({0}, opt);
	}
	return out.dn_crds(p[0], p[1], method, true, setNA, unit, opt);
}

//=== nearest (end) ==================================

