sgsPy
structurally guided sampling
Loading...
Searching...
No Matches
systematic.h
Go to the documentation of this file.
1/******************************************************************************
2 *
3 * Project: sgs
4 * Purpose: C++ implementation of systematic sampling
5 * Author: Joseph Meyer
6 * Date: July, 2025
7 *
8 ******************************************************************************/
9
14
15#include <iostream>
16#include <random>
17#include <optional>
18
19#include "utils/access.h"
20#include "utils/existing.h"
21#include "utils/helper.h"
22#include "utils/raster.h"
23#include "utils/vector.h"
24
25namespace sgs {
26namespace systematic {
27
47OGRGeometry *getAccessPolygon(vector::GDALVectorWrapper *p_access, std::string layerName, double buffInner, double buffOuter) {
48 //step 1: create multipolygon buffers
49 OGRMultiPolygon *buffInnerPolygons = new OGRMultiPolygon;
50 OGRMultiPolygon *buffOuterPolygons = new OGRMultiPolygon;
51 OGRGeometry *p_polygonMask;
52
53 //step 2: add geometries to access polygon buffers
54 for (const auto& p_feature : *p_access->getLayer(layerName.c_str())) {
55 OGRGeometry *p_geometry = p_feature->GetGeometryRef();
56 if (!p_geometry) {
57 throw std::runtime_error(std::format("An invalid geometry was encountered in the {} layer.", layerName));
58 }
59
60 OGRwkbGeometryType type = wkbFlatten(p_geometry->getGeometryType());
61
62 switch (type) {
63 case OGRwkbGeometryType::wkbLineString: {
64 OGRGeometry *p_outer = p_geometry->Buffer(buffOuter);
65 buffOuterPolygons->addGeometry(p_outer);
66 OGRGeometryFactory::destroyGeometry(p_outer);
67
68 if (buffInner != 0) {
69 OGRGeometry *p_inner = p_geometry->Buffer(buffInner);
70 buffInnerPolygons->addGeometry(p_inner);
71 OGRGeometryFactory::destroyGeometry(p_inner);
72 }
73 break;
74 }
75 case OGRwkbGeometryType::wkbMultiLineString: {
76 for (const auto& p_lineString : *p_geometry->toMultiLineString()) {
77 OGRGeometry *p_outer = p_geometry->Buffer(buffOuter);
78 buffOuterPolygons->addGeometry(p_outer);
79 OGRGeometryFactory::destroyGeometry(p_outer);
80 if (buffInner != 0) {
81 OGRGeometry *p_inner = p_geometry->Buffer(buffInner);
82 buffInnerPolygons->addGeometry(p_inner);
83 OGRGeometryFactory::destroyGeometry(p_inner);
84 }
85 }
86 break;
87 }
88 default: {
89 throw std::runtime_error("access polygon geometry type must be LineString or MultiLineString");
90 }
91 }
92 }
93
94 //step 3: generate the polygon mask and free no longer used memory
95 if (buffInner == 0) {
96 p_polygonMask = buffOuterPolygons->UnionCascaded();
97 }
98 else {
99 OGRGeometry *buffOuterUnion = buffOuterPolygons->UnionCascaded();
100 OGRGeometry *buffInnerUnion = buffInnerPolygons->UnionCascaded();
101 p_polygonMask = buffOuterUnion->Difference(buffInnerUnion);
102 OGRGeometryFactory::destroyGeometry(buffOuterUnion);
103 OGRGeometryFactory::destroyGeometry(buffInnerUnion);
104 }
105 delete buffOuterPolygons;
106 delete buffInnerPolygons;
107
108 return p_polygonMask;
109}
110
124inline bool
125checkExtent(double x, double y, double xMin, double xMax, double yMin, double yMax) {
126 return (x >= xMin && x <= xMax && y >= yMin && y <= yMax);
127}
128
138inline bool
139checkAccess(OGRPoint *p_point, OGRGeometry *p_geometry) {
140 return !p_geometry || p_point->Within(p_geometry);
141}
142
153inline bool
155 return !existing.used || !existing.containsCoordinates(x, y);
156}
157
178inline bool
179checkNotNan(raster::GDALRasterWrapper *p_raster, double *IGT, double xCoord, double yCoord, bool force) {
180 if (force) {
181 int x = static_cast<int>(IGT[0] + xCoord * IGT[1] + yCoord * IGT[2]);
182 int y = static_cast<int>(IGT[3] + xCoord * IGT[4] + yCoord * IGT[5]);
183
184 for (int i = 0; i < p_raster->getBandCount(); i++) {
185 GDALRasterBand *p_band = p_raster->getRasterBand(i);
186
187 double val;
188 OGRErr err = p_band->RasterIO(GF_Read, x, y, 1, 1, &val, 1, 1, GDT_Float64, 0, 0);
189 if (err) {
190 throw std::runtime_error("could not read value from raster.");
191 }
192
193 if (val == p_band->GetNoDataValue() || std::isnan(val)) {
194 return false;
195 }
196 }
197 }
198
199 return true;
200}
201
247std::tuple<
248 vector::GDALVectorWrapper *, //GDALDataset containing sample points
249 std::vector<std::vector<double>>, //array of samples to plot
250 std::vector<std::vector<std::vector<double>>> //array of grid to plot
251>
254 double cellSize,
255 std::string shape,
256 std::string location,
257 vector::GDALVectorWrapper *p_existing,
259 std::string layerName,
260 double buffInner,
261 double buffOuter,
262 bool force,
263 bool plot,
264 std::optional<uint64_t> random_state,
265 std::string filename)
266{
267 GDALAllRegister();
268
269 double *GT; //geotransform
270 double IGT[6]; //inverse geotransform
271 GT = p_raster->getGeotransform();
272 GDALInvGeoTransform(GT, IGT);
273
274 //determine raster extent
275 double xMin, xMax, yMin, yMax;
276 xMin = p_raster->getXMin();
277 xMax = p_raster->getXMax();
278 yMin = p_raster->getYMin();
279 yMax = p_raster->getYMax();
280
281 //generate random number generator
282 double xDiff = xMax - xMin;
283 double yDiff = yMax - yMin;
284 double rngMax = yDiff * xDiff;
285 std::mt19937 engine(random_state.value_or(std::random_device{}()));
286 std::uniform_real_distribution<double> dist(0.0, rngMax);
287 auto rng = [&] { return dist(engine); };
288
289 //determine random rotation angle within extent polygon
290 double rotation = rng() / (rngMax / 180);
291
292 //determine grid creation function
293 std::string gridFunction;
294 if (shape == "square") {
295 gridFunction = "ST_SquareGrid";
296 }
297 else { //shape == "hexagon"
298 gridFunction = "ST_HexagonalGrid";
299 }
300
301 //create sql query using extent polygon and grid function
302 std::string extentPolygon = "'POLYGON (( "
303 + std::to_string(xMin) + " " + std::to_string(yMin) + ", "
304 + std::to_string(xMin) + " " + std::to_string(yMax) + ", "
305 + std::to_string(xMax) + " " + std::to_string(yMax) + ", "
306 + std::to_string(xMax) + " " + std::to_string(yMin) + ", "
307 + std::to_string(xMin) + " " + std::to_string(yMin) + " ))'";
308
309 std::string queryString = "SELECT RotateCoords("
310 + gridFunction
311 + "(RotateCoords("
312 + "ST_GeomFromText("
313 + extentPolygon
314 + "), " + std::to_string(-rotation) + "), "
315 + std::to_string(cellSize) + "), "
316 + std::to_string(rotation) + ")";
317
318 //query to create grid
319 OGRLayer *p_grid = p_raster->getDataset()->ExecuteSQL(queryString.c_str(), nullptr, "SQLITE");
320
321 //create output dataset before anything that might take a long time (in case creation of dataset fails)
322 GDALDriver *p_driver = GetGDALDriverManager()->GetDriverByName("MEM");
323 if(!p_driver) {
324 throw std::runtime_error("unable to create output sample dataset driver.");
325 }
326 GDALDataset *p_sampleDataset = p_driver->Create("", 0, 0, 0, GDT_Unknown, nullptr);
327 if (!p_sampleDataset) {
328 throw std::runtime_error("unable to create output dataset with driver.");
329 }
330
331 vector::GDALVectorWrapper *p_wrapper = new vector::GDALVectorWrapper(p_sampleDataset, std::string(p_raster->getDataset()->GetProjectionRef()));
332 OGRLayer *p_sampleLayer = p_sampleDataset->CreateLayer("samples", p_wrapper->getSRS(), wkbPoint, nullptr);
333 if (!p_sampleLayer) {
334 throw std::runtime_error("unable to create output dataset layer.");
335 }
336
337 //get access polygon if access is given
338 OGRGeometry *access = nullptr;
339 if (p_access) {
340 //occasionally, samples end up being placed just outside the accessible area,
341 //typically when the buffer sizes are multiples of the pixel size.
342 //
343 //Adjusting the buffers minorly fixes this problem.
344 double pixelSize = std::min(p_raster->getPixelHeight(), p_raster->getPixelWidth());
345 buffOuter = buffOuter - pixelSize / 50;
346 buffInner = buffInner == 0 ? 0 : buffInner + pixelSize / 50;
347
348 access = getAccessPolygon(p_access, layerName, buffInner, buffOuter);
349 }
350
351 //coordinate representations the samples as vectors only if PLOT is true, returned to the (Python) caller
352 std::vector<double> xCoords, yCoords;
353
354 //create existing struct
355 existing::Existing existing(p_existing, p_raster, GT, p_raster->getWidth(), p_sampleLayer, plot, xCoords, yCoords);
356 helper::Field fieldExistingFalse("existing", 0);
357
358 //grid represents the grid used to create the sampels only if PLOT is true, and is returned to the (Python) caller
359 std::vector<std::vector<std::vector<double>>> grid;
360
361 //iterate through the polygons in the grid and populate the ruturn data depending on user inputs
362 for (const auto& p_feature : *p_grid) {
363 OGRGeometry *p_geometry = p_feature->GetGeometryRef();
364 if (!p_geometry) {
365 throw std::runtime_error("An invalid geometry was encountered in the grid. This is a bug and should be reported to the sgspy github page.");
366 }
367 for (const auto& p_polygon : *p_geometry->toMultiPolygon()) {
368 //generate sample depending on 'location' parameter
369 OGRPoint point;
370 OGRPoint secondPoint;
371 if (location == "centers") {
372 p_polygon->Centroid(&point);
373
374 double x = point.getX();
375 double y = point.getY();
376 if (checkExtent(x, y, xMin, xMax, yMin, yMax) &&
377 checkAccess(&point, access) &&
378 checkExisting(x, y, existing) &&
379 checkNotNan(p_raster, IGT, x, y, force))
380 {
381 existing.used ?
382 helper::addPoint(&point, p_sampleLayer, &fieldExistingFalse) :
383 helper::addPoint(&point, p_sampleLayer);
384
385 if (plot) {
386 xCoords.push_back(x);
387 yCoords.push_back(y);
388 }
389 }
390 }
391 else if (location == "corners") {
392 (*p_polygon->begin())->getPoint(0, &point);
393 (*p_polygon->begin())->getPoint(1, &secondPoint);
394
395 double x = point.getX();
396 double y = point.getY();
397 if (checkExtent(x, y, xMin, xMax, yMin, yMax) &&
398 checkAccess(&point, access) &&
399 checkExisting(x, y, existing) &&
400 checkNotNan(p_raster, IGT, x, y, force))
401 {
402 existing.used ?
403 helper::addPoint(&point, p_sampleLayer, &fieldExistingFalse) :
404 helper::addPoint(&point, p_sampleLayer);
405
406 if (plot) {
407 xCoords.push_back(x);
408 yCoords.push_back(y);
409 }
410 }
411
412 x = secondPoint.getX();
413 y = secondPoint.getY();
414 if (checkExtent(x, y, xMin, xMax, yMin, yMax) &&
415 checkAccess(&secondPoint, access) &&
416 checkExisting(x, y, existing) &&
417 checkNotNan(p_raster, IGT, x, y, force))
418 {
419 existing.used ?
420 helper::addPoint(&point, p_sampleLayer, &fieldExistingFalse) :
421 helper::addPoint(&secondPoint, p_sampleLayer);
422
423 if (plot) {
424 xCoords.push_back(x);
425 yCoords.push_back(y);
426 }
427 }
428
429 }
430 else { //location == "random"
431 OGREnvelope envelope;
432 p_polygon->getEnvelope(&envelope);
433 double xMinEnv = envelope.MinX;
434 double xMaxEnv = envelope.MaxX;
435 double xDiffEnv = xMaxEnv - xMinEnv;
436 double yMinEnv = envelope.MinY;
437 double yMaxEnv = envelope.MaxY;
438 double yDiffEnv = yMaxEnv - yMinEnv;
439
440 int tries = 0;
441 bool found = false;
442
443 while (tries < 10 && !found) {
444 point.setX(xMinEnv + rng() / (rngMax / xDiffEnv));
445 point.setY(yMinEnv + rng() / (rngMax / yDiffEnv));
446 while (!p_polygon->Contains(&point)) {
447 point.setX(xMinEnv + rng() / (rngMax / xDiffEnv));
448 point.setY(yMinEnv + rng() / (rngMax / yDiffEnv));
449 }
450
451 double x = point.getX();
452 double y = point.getY();
453 if (checkExtent(x, y, xMin, xMax, yMin, yMax) &&
454 checkAccess(&point, access) &&
455 checkExisting(x, y, existing) &&
456 checkNotNan(p_raster, IGT, x, y, force))
457 {
458 found = true;
459 existing.used ?
460 helper::addPoint(&point, p_sampleLayer, &fieldExistingFalse) :
461 helper::addPoint(&point, p_sampleLayer);
462
463 if (plot) {
464 xCoords.push_back(x);
465 yCoords.push_back(y);
466 }
467 }
468
469 tries++;
470 }
471 }
472
473 //set grid vector to be plot
474 if (plot) {
475 grid.push_back({}); //add new polygon to grid
476 grid.back().push_back({}); //add new x vector to polygon grid
477 grid.back().push_back({}); //add new y vector to polygon grid
478 for (const auto& p_linearRing : *p_polygon) {
479 for (const auto& p_point : *p_linearRing) {
480 grid.back()[0].push_back(p_point.getX());
481 grid.back()[1].push_back(p_point.getY());
482 }
483 }
484 }
485 }
486 }
487
488 if (filename != "") {
489 try {
490 p_wrapper->write(filename);
491 }
492 catch (const std::exception& e) {
493 std::cout << "Exception thrown trying to write file: " << e.what() << std::endl;
494 }
495 }
496
497 //clean up memory
498 p_raster->getDataset()->ReleaseResultSet(p_grid);
499 if (p_access) {
500 OGRGeometryFactory::destroyGeometry(access);
501 }
502
503 return {p_wrapper, {xCoords, yCoords}, grid};
504}
505
506} //namespace systematic
507} //namespace sgs
Definition raster.h:57
double getYMax()
Definition raster.h:525
GDALDataset * getDataset()
Definition raster.h:429
int getWidth()
Definition raster.h:467
double * getGeotransform()
Definition raster.h:590
int getBandCount()
Definition raster.h:485
double getXMax()
Definition raster.h:495
GDALRasterBand * getRasterBand(int band)
Definition raster.h:696
double getXMin()
Definition raster.h:510
double getPixelWidth()
Definition raster.h:555
double getYMin()
Definition raster.h:540
double getPixelHeight()
Definition raster.h:565
Definition vector.h:46
OGRSpatialReference * getSRS(void)
Definition vector.h:470
OGRLayer * getLayer(std::string layerName)
Definition vector.h:250
void write(std::string filename)
Definition vector.h:410
void addPoint(OGRPoint *p_point, OGRLayer *p_layer)
Definition helper.h:700
bool checkExisting(double x, double y, existing::Existing &existing)
Definition systematic.h:154
bool checkNotNan(raster::GDALRasterWrapper *p_raster, double *IGT, double xCoord, double yCoord, bool force)
Definition systematic.h:179
bool checkExtent(double x, double y, double xMin, double xMax, double yMin, double yMax)
Definition systematic.h:125
OGRGeometry * getAccessPolygon(vector::GDALVectorWrapper *p_access, std::string layerName, double buffInner, double buffOuter)
Definition systematic.h:47
bool checkAccess(OGRPoint *p_point, OGRGeometry *p_geometry)
Definition systematic.h:139
std::tuple< vector::GDALVectorWrapper *, std::vector< std::vector< double > >, std::vector< std::vector< std::vector< double > > > > systematic(raster::GDALRasterWrapper *p_raster, double cellSize, std::string shape, std::string location, vector::GDALVectorWrapper *p_existing, vector::GDALVectorWrapper *p_access, std::string layerName, double buffInner, double buffOuter, bool force, bool plot, std::optional< uint64_t > random_state, std::string filename)
Definition systematic.h:252
Definition access.h:23
Definition dist.h:20
Definition existing.h:27
Definition pca.h:23
Definition existing.h:38
Definition helper.h:46