// 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 "spatVector.h"
#include "string_utils.h"
#include "vecmath.h"
#include "recycle.h"
#include "distance.h"
#include <functional>


/*
std::vector<bool> SpatVector::is_valid() {
	std::vector<bool> out;
	out.reserve(nrow());
	GDALDataset* src;
	if (!write_ogr(src, "", "layer", "Memory", false, false, true, std::vector<std::string>())) {
		if (src != NULL) GDALClose( src );
		setError("cannot do it");
		return false;
	}
	OGRLayer *inLayer = src->GetLayer(0);
	inLayer->ResetReading();
	OGRFeature *inFeature;
	while( (inFeature = inLayer->GetNextFeature()) != NULL ) {
		OGRGeometry *poGeometry = inFeature->GetGeometryRef();
		out.push_back(poGeometry->IsValid());
		OGRFeature::DestroyFeature( inFeature );
	}
	return out;
}


SpatVector SpatVector::make_valid() {
	SpatVector out;
	GDALDataset* src = write_ogr("", "layer", "Memory", false, false, true, std::vector<std::string>());
	OGRLayer *inLayer = src->GetLayer(0);
	inLayer->ResetReading();
	OGRFeature *inFeature;
	while( (inFeature = inLayer->GetNextFeature()) != NULL ) {
		OGRGeometry *poGeometry = inFeature->GetGeometryRef();
		//OGRGeometry *poGeom = poGeometry->MakeValid();
		if (inFeature->SetGeometry( poGeometry ) != OGRERR_NONE) {
			out.setError("cannot set geometry");
			return out;
		}
		if (inLayer->SetFeature( inFeature ) != OGRERR_NONE) {
			out.setError("cannot set feature");
			return out;
		}
		OGRFeature::DestroyFeature( inFeature );
	}
	std::vector<double> fext;
	SpatVector fvct;
	out.read_ogr(src, "", "", fext, fvct, false, "");
	GDALClose(src);
	return out;
}
*/


SpatVector SpatVector::disaggregate(bool segments) {

	SpatVector out;
	out.srs = srs;
	out.df = df.skeleton();
	if (nrow() == 0) {
		return out;
	}
	size_t n=0;
	for (size_t i=0; i<nrow(); i++) {
		n += geoms[i].parts.size();
	}
	out.reserve(n);

	for (size_t i=0; i<nrow(); i++) {
		SpatGeom g = getGeom(i);
		SpatDataFrame row = df.subset_rows(i);
		for (size_t j=0; j<g.parts.size(); j++) {
			SpatGeom gg = SpatGeom(g.parts[j], g.gtype);
			out.addGeom(gg);
			if (!out.df.rbind(row)) {
				out.setError("cannot add row");
				return out;
			}
		}
	}
	if (segments && (type() != "points")) {
		SpatVector x;
		x.srs = srs;
		x.df = df.skeleton();

		for (size_t i=0; i<out.nrow(); i++) {
			SpatGeom g = out.getGeom(i);
			SpatDataFrame row = out.df.subset_rows(i);
			size_t n = g.parts[0].x.size() - 1;
			for (size_t j=0; j<n; j++) {
				std::vector<double> sx = {g.parts[0].x[j], g.parts[0].x[j+1]};
				std::vector<double> sy = {g.parts[0].y[j], g.parts[0].y[j+1]};
				SpatPart p(sx, sy);
				SpatGeom gg = SpatGeom(p, lines);
				x.addGeom(gg);
				if (!x.df.rbind(row)) {
					x.setError("cannot add row");
					return x;
				}
			}
		}
		return x;
	}

	return out;

}


SpatVector SpatVector::aggregate(std::string field, bool dissolve) {

	SpatVector out;
	int i = where_in_vector(field, get_names(), false);
	if (i < 0) {
		out.setError("cannot find field: " + field);
		return out;
	}
	SpatDataFrame uv;
	std::vector<int> idx = df.getIndex(i, uv);
	out.reserve(uv.nrow());
	for (size_t i=0; i<uv.nrow(); i++) {
		SpatGeom g;
		g.gtype = geoms[0].gtype;
		for (size_t j=0; j<idx.size(); j++) {
			if (i == (size_t)idx[j]) {
				g.unite( getGeom(j) );
			}
		}
		out.addGeom(g);
	}
	if (dissolve) {
		out = out.unaryunion();
	}
	out.srs = srs;
	out.df  = uv;
	return out;
}



SpatVector SpatVector::aggregate(bool dissolve) {
	SpatVector out;
	SpatGeom g;
	if (geoms.empty()) {
		out = *this;
		return out;
	}
	g.gtype = geoms[0].gtype;
	for (size_t i=0; i<size(); i++) {
		g.unite( getGeom(i) );
	}
	out.addGeom(g);
	if (dissolve) {
		out = out.unaryunion();
	}
	out.srs = srs;
	return out;
}


#include "geodesic.h"

void extend_line(const double &x1, const double &y1, double &x2, double &y2, const bool &geo, const double &distance) {
	if (geo) {
		double a = 6378137.0;
		double f = 1/298.257223563;
		double s12, azi1, azi2;
		struct geod_geodesic g;
		geod_init(&g, a, f);
		geod_inverse(&g, y1, x1, y2, x2, &s12, &azi1, &azi2);		
		geod_direct(&g, y2, x2, azi2, distance, &y2, &x2, &azi1);
	} else {
		double bearing;
		double dx = x2 - x1;
		double dy = y2 - y1;
		if (dx == 0) {
			if (y2 > y1) {
				bearing = -M_PI / 2;
			} else {
				bearing = M_PI / 2;				
			}
		} else {
			bearing = atan(dy/dx);
		}
		if (x2 > x1) {
			x2 += distance * cos(bearing);
			y2 += distance * sin(bearing);			
		} else {
			x2 -= distance * cos(bearing);			
			y2 -= distance * sin(bearing);						
		}
	}
}

SpatVector SpatVector::elongate(double length, bool flat) {

	SpatVector out = *this;
	size_t n = size();
	if (n == 0) {
		return out;
	}
	if (geoms[0].gtype != lines) {
		out.setError("you can only elongate lines");
		return out;
	}
	if (length < 0) {
		out.setError("length must be > 0");
		return out;
	}
	if (length == 0) {
		return out;
	}

	bool geo = (!flat) && is_lonlat();

	for (size_t i=0; i<n; i++) {
		for (size_t j=0; j < out.geoms[i].size(); j++) {
			SpatPart p = out.geoms[i].parts[j];
			size_t pn = p.x.size();
			if (pn < 2) continue;
			extend_line(p.x[1]   , p.y[1]   , p.x[0] , p.y[0] , geo, length);
			extend_line(p.x[pn-2], p.y[pn-2], p.x[pn-1], p.y[pn-1], geo, length);		
			out.geoms[i].parts[j] = p;
		}
		out.geoms[i].computeExtent();
	}
	out.computeExtent();
	return out;
}

SpatVectorCollection SpatVector::split(std::string field) {

	SpatVectorCollection out;

	int i = where_in_vector(field, get_names(), false);
	if (i < 0) {
		out.setError("cannot find field: " + field);
		return out;
	}
	SpatDataFrame uv;
	std::vector<int> idx = df.getIndex(i, uv);

	for (size_t i=0; i<uv.nrow(); i++) {
		SpatVector v;
		std::vector<size_t> r;
		for (size_t j=0; j<idx.size(); j++) {
			if (i == (size_t)idx[j]) {
				v.addGeom( getGeom(j) );
				r.push_back(j);
			}
		}
		v.srs = srs;
		v.df = df.subset_rows(r);
		out.push_back(v);
	}
	out.names = uv.as_string(0);
	return out;
}



SpatVector SpatVector::remove_holes() {

	SpatVector out = *this;

	size_t n = size();
	if (n == 0) {
		return out;
	}
	if (geoms[0].gtype != polygons) {
		return out;
	}

	for (size_t i=0; i<n; i++) {
		for (size_t j=0; j < out.geoms[i].size(); j++) {
			SpatPart p = out.geoms[i].parts[j];
			if (p.hasHoles()) {
				p.holes.resize(0);
				out.geoms[i].parts[j] = p;
			}
		}
	}
	return out;
}


SpatVector SpatVector::get_holes() {

	SpatVector out;
	out.srs = srs;
	size_t n = size();
	if (n == 0) {
		return out;
	}
	if (geoms[0].gtype != polygons) {
		return out;
	}
	std::vector<size_t> atts;

	for (size_t i=0; i<n; i++) {
		SpatGeom g;
		g.gtype = polygons;
		bool found = false;
		for (size_t j=0; j < geoms[i].size(); j++) {
			SpatPart p = geoms[i].parts[j];
			if (p.hasHoles()) {
				for (size_t k=0; k < p.nHoles(); k++) {
					if (p.holes[k].hasZ()) {
						SpatPart h(p.holes[k].x, p.holes[k].y, p.holes[k].z);
						g.addPart(h);
					} else {
						SpatPart h(p.holes[k].x, p.holes[k].y);
						g.addPart(h);
					}
				}
				found = true;
			}
		}
		if (found) {
			out.addGeom(g);
			atts.push_back(i);
		}
	}
	out.df = df.subset_rows(atts);
	return out;
}


SpatVector SpatVector::set_holes(SpatVector x, size_t i) {

	SpatVector out;
	if (size() == 0) {
		out.setError("object has no geometries");
		return out;
	}
	if (i > size()) {
		out.setError("invalid index");
		return out;
	}
	if (x.type() != "polygons") {
		out.setError("holes must be polygons");
		return out;
	}
	if (out.geoms[i].size() > 1) {
		out.setError("selected object has multiple geometries");
	}

	x = x.unaryunion();
	SpatPart p  = out.geoms[i].parts[0];
	SpatGeom g =   x.geoms[0];
	for (size_t i=0; i<g.size(); i++) {
		if (g.parts[i].hasZ()) {
			p.addHole(g.parts[i].x, g.parts[i].y, g.parts[i].z);
		} else {
			p.addHole(g.parts[i].x, g.parts[i].y);
		}
	}
	out = *this;
	out.geoms[i].parts[0] = p;
	return out;
}




/*
std::vector<OGRGeometry *> geoms_from_ds(GDALDataset* src, int field, int value) {
	std::vector<OGRGeometry *> g;
	OGRLayer *poLayer = src->GetLayer(0);
	poLayer->ResetReading();
	OGRFeature *poFeature;

	while( (poFeature = poLayer->GetNextFeature()) != NULL ) {
		OGRGeometry *poGeometry = poFeature->GetGeometryRef();
		g.push_back(poGeometry);
	}
	return g;
}
// create output dataset
	GDALDataset* dst;
// get unique values in field
// loop over unique values
	// for value in uvalues
	std::vector<OGRGeometry *> gvec = geoms_from_ds(src, field, value);
	OGRGeometry *geom;
	geom = (OGRGeometry *) gvec.data();
	OGRGeometry *gout;
	gout = geom->UnionCascaded();
// set geometry to output
   return dst;
*/




SpatVector SpatVector::shift(double x, double y) {

	SpatVector out = *this;

	for (size_t i=0; i < size(); i++) {
		for (size_t j=0; j < geoms[i].size(); j++) {
			for (size_t q=0; q < geoms[i].parts[j].x.size(); q++) {
				out.geoms[i].parts[j].x[q] += x;
				out.geoms[i].parts[j].y[q] += y;
			}
			if (geoms[i].parts[j].hasHoles()) {
				for (size_t k=0; k < geoms[i].parts[j].nHoles(); k++) {
					for (size_t q=0; q < geoms[i].parts[j].holes[k].x.size(); q++) {
						out.geoms[i].parts[j].holes[k].x[q] += x;
						out.geoms[i].parts[j].holes[k].y[q] += y;
					}
					out.geoms[i].parts[j].holes[k].extent.xmin += x;
					out.geoms[i].parts[j].holes[k].extent.xmax += x;
					out.geoms[i].parts[j].holes[k].extent.ymin += y;
					out.geoms[i].parts[j].holes[k].extent.ymax += y;
				}
			}
			out.geoms[i].parts[j].extent.xmin += x;
			out.geoms[i].parts[j].extent.xmax += x;
			out.geoms[i].parts[j].extent.ymin += y;
			out.geoms[i].parts[j].extent.ymax += y;
		}
		out.geoms[i].extent.xmin += x;
		out.geoms[i].extent.xmax += x;
		out.geoms[i].extent.ymin += y;
		out.geoms[i].extent.ymax += y;
	}
	out.extent.xmin += x;
	out.extent.xmax += x;
	out.extent.ymin += y;
	out.extent.ymax += y;
	return out;
}


void resc(double &value, const double &base, const double &f) {
	value = base + f * (value - base);
}


SpatVector SpatVector::rescale(double fx, double fy, double x0, double y0) {

	SpatVector out = *this;
	for (size_t i=0; i < size(); i++) {
		for (size_t j=0; j < geoms[i].size(); j++) {
			for (size_t q=0; q < geoms[i].parts[j].x.size(); q++) {
				resc(out.geoms[i].parts[j].x[q], x0, fx);
				resc(out.geoms[i].parts[j].y[q], y0, fy);
			}
			if (geoms[i].parts[j].hasHoles()) {
				for (size_t k=0; k < geoms[i].parts[j].nHoles(); k++) {
					for (size_t q=0; q < geoms[i].parts[j].holes[k].x.size(); q++) {
						resc(out.geoms[i].parts[j].holes[k].x[q], x0, fx);
						resc(out.geoms[i].parts[j].holes[k].y[q], y0, fy);
					}
					resc(out.geoms[i].parts[j].holes[k].extent.xmax, x0, fx);
					resc(out.geoms[i].parts[j].holes[k].extent.ymax, y0, fy);
				}
			}
			resc(out.geoms[i].parts[j].extent.xmin, x0, fx);
			resc(out.geoms[i].parts[j].extent.xmax, x0, fx);
			resc(out.geoms[i].parts[j].extent.ymin, y0, fy);
			resc(out.geoms[i].parts[j].extent.ymax, y0, fy);
		}
		resc(out.geoms[i].extent.xmin, x0, fx);
		resc(out.geoms[i].extent.xmax, x0, fx);
		resc(out.geoms[i].extent.ymin, y0, fy);
		resc(out.geoms[i].extent.ymax, y0, fy);
	}
	resc(out.extent.xmin, x0, fx);
	resc(out.extent.xmax, x0, fx);
	resc(out.extent.ymin, y0, fy);
	resc(out.extent.ymax, y0, fy);
	return out;
}

void dswap(double &a, double&b) {
	double tmp = a;
	a = b;
	b = tmp;
}

SpatVector SpatVector::transpose() {

	SpatVector out = *this;
	for (size_t i=0; i < size(); i++) {
		for (size_t j=0; j < geoms[i].size(); j++) {
			out.geoms[i].parts[j].x.swap(out.geoms[i].parts[j].y);
			if (geoms[i].parts[j].hasHoles()) {
				for (size_t k=0; k < geoms[i].parts[j].nHoles(); k++) {
					out.geoms[i].parts[j].holes[k].x.swap(out.geoms[i].parts[j].holes[k].y);

					dswap(out.geoms[i].parts[j].holes[k].extent.xmin,
						 out.geoms[i].parts[j].holes[k].extent.ymin);
					dswap(out.geoms[i].parts[j].holes[k].extent.xmax,
						 out.geoms[i].parts[j].holes[k].extent.ymax);
				}
			}
			dswap(out.geoms[i].parts[j].extent.xmin,
				 out.geoms[i].parts[j].extent.ymin);
			dswap(out.geoms[i].parts[j].extent.xmax,
				 out.geoms[i].parts[j].extent.ymax);
		}
		dswap(out.geoms[i].extent.xmin, out.geoms[i].extent.ymin);
		dswap(out.geoms[i].extent.xmax, out.geoms[i].extent.ymax);
	}
	dswap(out.extent.xmin, out.extent.ymin);
	dswap(out.extent.xmax, out.extent.ymax);
	return out;
}


void flipd(double &value, const double &base) {
	value = base - (value - base);
}

void flipv(std::vector<double> &v, const double &base) {
	for (double &d : v) d = base - (d - base);
}

SpatVector SpatVector::flip(bool vertical) {
	double x0 = extent.xmin;
	double y0 = extent.ymin;
	SpatVector out = *this;
	bool horizontal = !vertical;
	for (size_t i=0; i < size(); i++) {
		for (size_t j=0; j < geoms[i].size(); j++) {
			if (horizontal) {
				flipv(out.geoms[i].parts[j].x, x0);
				flipd(out.geoms[i].parts[j].extent.xmin, x0);
				flipd(out.geoms[i].parts[j].extent.xmax, x0);
				dswap(out.geoms[i].parts[j].extent.xmin, out.geoms[i].parts[j].extent.xmax);
			} else {
				flipv(out.geoms[i].parts[j].y, y0);
				flipd(out.geoms[i].parts[j].extent.ymin, y0);
				flipd(out.geoms[i].parts[j].extent.ymax, y0);
				dswap(out.geoms[i].parts[j].extent.ymin, out.geoms[i].parts[j].extent.ymax);
			}
			if (geoms[i].parts[j].hasHoles()) {
				for (size_t k=0; k < geoms[i].parts[j].nHoles(); k++) {
					if (horizontal) {
						flipv(out.geoms[i].parts[j].holes[k].x, x0);
						flipd(out.geoms[i].parts[j].holes[k].extent.xmin, x0);
						flipd(out.geoms[i].parts[j].holes[k].extent.xmax, x0);
						dswap(out.geoms[i].parts[j].holes[k].extent.xmin,
							  out.geoms[i].parts[j].holes[k].extent.xmax);
					} else {
						flipv(out.geoms[i].parts[j].holes[k].y, y0);
						flipd(out.geoms[i].parts[j].holes[k].extent.ymin, y0);
						flipd(out.geoms[i].parts[j].holes[k].extent.ymax, y0);
						dswap(out.geoms[i].parts[j].holes[k].extent.ymin,
							  out.geoms[i].parts[j].holes[k].extent.ymax);
					}
				}
			}
		}
		if (horizontal) {
			flipd(out.geoms[i].extent.xmin, x0);
			flipd(out.geoms[i].extent.xmax, x0);
			dswap(out.geoms[i].extent.xmin, out.geoms[i].extent.xmax);
		} else {
			flipd(out.geoms[i].extent.ymin, y0);
			flipd(out.geoms[i].extent.ymax, y0);
			dswap(out.geoms[i].extent.ymin, out.geoms[i].extent.ymax);
		}
	}
	if (horizontal) {
		flipd(out.extent.xmin, x0);
		flipd(out.extent.xmax, x0);
		dswap(out.extent.xmin, out.extent.xmax);
	} else {
		flipd(out.extent.ymin, y0);
		flipd(out.extent.ymax, y0);
		dswap(out.extent.ymin, out.extent.ymax);
	}
	return out;
}



void rotit(std::vector<double> &x, std::vector<double> &y, const double &x0, const double &y0, const double &cos_angle, const double &sin_angle) {
	for (size_t i=0; i<x.size(); i++) {
		double sx = x[i] - x0;
		double sy = y[i] - y0;
		x[i] = sx * cos_angle + sy * -sin_angle + x0;
		y[i] = sx * sin_angle + sy * cos_angle + y0;
	}
}

void rotit_geo(std::vector<double> &lon, std::vector<double> &lat, const double &lon0, const double &lat0, const double &angle, const double &angle2) {
	double a = 6378137.0;
	double f = 1/298.257223563;
	double s12, azi1, azi2;
	struct geod_geodesic g;
	geod_init(&g, a, f);
	double angle_rad = angle * 57.2957795130823 ;

	for (size_t i=0; i<lon.size(); i++) {
		geod_inverse(&g, lat0, lon0, lat[i], lon[i], &s12, &azi1, &azi2);
		geod_direct(&g, lat0, lon0, azi1 - angle_rad, s12, &lat[i], &lon[i], &azi2);
	}
}


SpatVector SpatVector::rotate(double angle, std::vector<double> x0, std::vector<double> y0) {
	angle = -M_PI * angle / 180;
	size_t n = size();
	if (x0.empty() || y0.empty()) {
		SpatVector out;
		out.setError("no center of rotation provided");
		return out;
	}
	bool multi = true;
	double ix0, iy0; 
	if ((x0.size() == 1) && (y0.size() == 1)) {
		multi = false;
		ix0 = x0[0];
		iy0 = y0[0]; 
	} else {
		recycle(x0, n);
		recycle(y0, n);
	}

	double cos_angle, sin_angle;
	std::function<void(std::vector<double>&, std::vector<double>&, const double&, const double&, const double&, const double&)>  rotate_it;
	if (is_lonlat()) {
		cos_angle = angle;
		sin_angle = angle;
		rotate_it = rotit_geo;
	} else {
		cos_angle = cos(angle);
		sin_angle = sin(angle);		
		rotate_it = rotit;
	}
	SpatVector out = *this;
	for (size_t i=0; i < n; i++) {
		if (multi) {
			ix0 = x0[i];
			iy0 = y0[i];
		} 
		for (size_t j=0; j < geoms[i].size(); j++) {
			rotate_it(out.geoms[i].parts[j].x, out.geoms[i].parts[j].y, ix0, iy0, cos_angle, sin_angle);
			if (geoms[i].parts[j].hasHoles()) {
				for (size_t k=0; k < geoms[i].parts[j].nHoles(); k++) {
					rotate_it(out.geoms[i].parts[j].holes[k].x,
						  out.geoms[i].parts[j].holes[k].y, ix0, iy0, cos_angle, sin_angle);

					out.geoms[i].parts[j].holes[k].extent.xmin =
						vmin(out.geoms[i].parts[j].holes[k].x, true);
					out.geoms[i].parts[j].holes[k].extent.xmax =
						vmax(out.geoms[i].parts[j].holes[k].x, true);
					out.geoms[i].parts[j].holes[k].extent.ymin =
						vmin(out.geoms[i].parts[j].holes[k].y, true);
					out.geoms[i].parts[j].holes[k].extent.ymax =
						vmax(out.geoms[i].parts[j].holes[k].y, true);
				}
			}
			out.geoms[i].parts[j].extent.xmin = vmin(out.geoms[i].parts[j].x, true);
			out.geoms[i].parts[j].extent.xmax = vmax(out.geoms[i].parts[j].x, true);
			out.geoms[i].parts[j].extent.ymin = vmin(out.geoms[i].parts[j].y, true);
			out.geoms[i].parts[j].extent.ymax = vmax(out.geoms[i].parts[j].y, true);
			if (j==0) {
				out.geoms[i].extent = out.geoms[i].parts[j].extent;
			} else {
				out.geoms[i].extent.unite(out.geoms[i].parts[j].extent);
			}
		}
		if (i==0) {
			out.extent = out.geoms[i].extent;
		} else {
			out.extent.unite(out.geoms[i].extent);
		}
	}
	return out;
}


inline double cartdist(const double& x1, const double& y1, const double &x2, const double &y2) {
	return sqrt(pow(x2-x1, 2) + pow(y2-y1, 2));
}



// Greedy node thinning along a single ring/line.
//
//   x, y     : in-place vertex sequence
//   thr      : threshold in the SAME units as `x`/`y`
//              (i.e. meters for lon/lat caller, CRS units for planar)
//   mnsize   : minimum number of vertices the result must have to be
//              accepted (3 for lines, 4 for closed rings)
//   lonlat   : if true, distances are computed geodesically (meters);
//              if false, planar Euclidean.
//   closed   : if true, the input is a closed ring (first==last) and the
//              last vertex is forced to mirror the first.
//
// Algorithm: walk forward keeping a "last kept" vertex. A new vertex is
// kept iff its distance to the last kept vertex exceeds `thr`. The very
// last input vertex is always kept (so polyline endpoints survive). On
// a closed ring the closing vertex is set equal to the first kept vertex.
//
// Returns true iff the geometry was actually shortened.
bool thinnodes(std::vector<double> &x, std::vector<double> &y,
               const double &thr, const size_t &mnsize,
               bool lonlat, bool closed) {
	const size_t n = x.size();
	if (n <= mnsize) return false;

	auto d2 = [&](size_t a, size_t b) {
		if (lonlat) {
			return distLonlat(x[a], y[a], x[b], y[b]);
		} else {
			double dx = x[a] - x[b];
			double dy = y[a] - y[b];
			return std::sqrt(dx*dx + dy*dy);
		}
	};

	std::vector<size_t> keep;
	keep.reserve(n);
	keep.push_back(0);
	const size_t last = n - 1;

	// Walk all interior vertices; never include `last` here -- we always
	// append it explicitly below so polyline endpoints / ring closures
	// are preserved.
	for (size_t i = 1; i < last; i++) {
		if (d2(i, keep.back()) > thr) {
			keep.push_back(i);
		}
	}
	// Always keep the last vertex -- but if the previous kept vertex is
	// already within `thr` of the last vertex, drop *it* in favour of
	// the endpoint (avoids leaving a sub-threshold tail).
	while (keep.size() > 1 && d2(last, keep.back()) <= thr) {
		keep.pop_back();
	}
	keep.push_back(last);

	// On a closed ring, force last == first to keep the ring closed.
	if (keep.size() < mnsize) return false;
	if (keep.size() == n) return false;

	std::vector<double> xout, yout;
	xout.reserve(keep.size());
	yout.reserve(keep.size());
	for (size_t k : keep) {
		xout.push_back(x[k]);
		yout.push_back(y[k]);
	}
	if (closed) {
		xout.back() = xout.front();
		yout.back() = yout.front();
	}

	x = std::move(xout);
	y = std::move(yout);
	return true;
}



SpatVector SpatVector::thin_nodes(double threshold, std::string unit) {

	SpatVector out;
	if (threshold <= 0) {
		out.setError("threshold (d) must be a positive number");
		return out;
	}
	if (geoms.empty()) {
		out = *this;
		return out;
	}
	bool isLines    = (geoms[0].gtype == lines);
	bool isPolygons = (geoms[0].gtype == polygons);
	if (!(isLines || isPolygons)) {
		out.setError("can only thin lines or polygons");
		return out;
	}
	size_t mnode = isPolygons ? 4 : 2;

	// Convert threshold to the same units as the coordinate values:
	//   - planar CRS  -> CRS distance units (m, ft, ...): srs.m_dist
	//     gives the multiplier from the user-supplied `unit` to those
	//     CRS units (so d_internal = d / m).
	//   - lon/lat     -> meters: distLonlat returns meters, and m_dist
	//     converts the user `unit` to meters too.
	bool lonlat = is_lonlat();
	double m = 1;
	if (!srs.m_dist(m, lonlat, unit)) {
		out.setError("invalid unit");
		return out;
	}
	double d_internal = threshold / m;

	out = *this;
	bool objext = false;
	for (size_t i=0; i < out.size(); i++) {
		bool geomext = false;
		for (size_t j=0; j < out.geoms[i].size(); j++) {
			if (thinnodes(out.geoms[i].parts[j].x, out.geoms[i].parts[j].y,
			              d_internal, mnode, lonlat, isPolygons)) {
				geomext = true;
			}
			if (out.geoms[i].parts[j].hasHoles()) {
				for (size_t k=0; k < out.geoms[i].parts[j].nHoles(); k++) {
					if (thinnodes(out.geoms[i].parts[j].holes[k].x,
					              out.geoms[i].parts[j].holes[k].y,
					              d_internal, mnode, lonlat, true)) {
						geomext = true;
					}
				}
			}
		}
		if (geomext) {
			objext = true;
			out.geoms[i].computeExtent();
		}
	}
	if (objext) {
		out.computeExtent();
	}

	return out;
}






/*
SpatVector SpatVector::removeSlivers(double dthres, double athres, size_t n) {

	SpatVector out;

	if (geoms.empty() || geoms[0].gtype != polygons) {
		out.setError("can only remove slivers from polygons");
		return out;
	}
	if ((dthres < 0) || (athres < 0)) {
		out.setError("thresholds must be a positive number");
		return out;
	}
	if (n < 2)) {
		out.setError("n must be at least 2");
		return out;
	}

	out = *this;
	bool objext = false;
	for (size_t i=0; i < size(); i++) {
		bool geomext = false;
		for (size_t j=0; j < out.geoms[i].size(); j++) {
			if (remove_slivers(out.geoms[i].parts[j].x, out.geoms[i].parts[j].y, threshold, mnode)) {
				geomext = true;
			}
			if (geoms[i].parts[j].hasHoles()) {
				for (size_t k=0; k < geoms[i].parts[j].nHoles(); k++) {
					remove_slivers(geoms[i].parts[j].holes[k].x, geoms[i].parts[j].holes[k].y, threshold, mnode);
				}
			}
		}
		if (geomext) {
			objext = true;
			geoms[i].computeExtent();
		}
	}
	if (objext) {
		computeExtent();
	}
	return out;
}
*/

