TerrainGen coverage


terraingen/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
85 / 95
89.5%
Branches:
65 / 77
84.4%
Functions:
16 / 18
88.9%

python/src/sources.cpp
Line Branch Exec Source
1 #include "bindings.h"
2 #include "common.h"
3 #include "trampolines.h"
4
5 #include "terraingen/sources/field.h"
6 #include "terraingen/sources/flat_source.h"
7 #include "terraingen/sources/geotiff_source.h"
8 #include "terraingen/sources/source.h"
9 #include "terraingen/sources/source_chain.h"
10 #include "terraingen/sources/synthetic_terrain.h"
11
12 #include <nanobind/operators.h>
13
14 #include <optional>
15 #include <utility>
16 #include <vector>
17
18 namespace terraingen::python {
19
20 namespace {
21
22 /// @brief A source written in Python: `grid()` and `height_at(x, y)` are required; `fill(heights)`
23 /// fills a whole raster at once, and `reference_point()` gives (x, y) noise is measured from.
24 class PySource : public Source {
25 public:
26 NB_TRAMPOLINE(Source);
27
28 10 RasterGrid Grid() const override {
29
2/2
✓ Branch 2 taken 10 times.
✓ Branch 5 taken 10 times.
60 NB_OVERRIDE_PURE_NAME("grid", Grid);
30 }
31
32 1 Vec2d ReferencePoint() const override {
33
1/1
✓ Branch 1 taken 1 time.
1 return CallOverride<Vec2d>(nb_trampoline, "reference_point").value_or(Vec2d{});
34 }
35
36 9 void Fill(Raster<float>& heights) const override {
37 18 if (CallOverrideIgnoringResult(nb_trampoline, "fill",
38
2/3
✓ Branch 1 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
9 ByReference<Raster<float>>{heights})) {
39 ✗ return;
40 }
41
2/2
✓ Branch 1 taken 969 times.
✓ Branch 2 taken 9 times.
978 for (std::size_t row = 0; row < heights.Height(); ++row) {
42
2/2
✓ Branch 1 taken 161673 times.
✓ Branch 2 taken 969 times.
162642 for (std::size_t col = 0; col < heights.Width(); ++col) {
43 161673 const Vec2d centre = heights.CellCenter(col, row);
44
1/1
✓ Branch 1 taken 161673 times.
161673 heights(col, row) = HeightAt(centre.x, centre.y);
45 }
46 }
47 }
48
49 161675 float HeightAt(double x, double y) const override {
50
2/2
✓ Branch 2 taken 161675 times.
✓ Branch 5 taken 161675 times.
970050 NB_OVERRIDE_PURE_NAME("height_at", HeightAt, x, y);
51 }
52 };
53
54 44 void BindSourceBase(nb::module_& m) {
55 44 nb::class_<Source, PySource>(
56 m, "Source",
57 "Where a terrain's heights come from, over its grid. Subclass it and implement `grid()` "
58 "(a RasterGrid) and `height_at(x, y)`, and optionally `fill(heights)` and "
59 "`reference_point()`, for a source of your own.")
60 44 .def(nb::init<>())
61 44 .def("grid", &Source::Grid)
62
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 .def("height_at", &Source::HeightAt, "x"_a, "y"_a)
63 44 .def("reference_point", &Source::ReferencePoint)
64
1/1
✓ Branch 1 taken 44 times.
88 .def("fill", &Source::Fill, "heights"_a, "Sets every cell of `heights`.")
65
1/1
✓ Branch 1 taken 6 times.
50 .def_prop_ro("width", [](const Source& source) { return source.Grid().width; })
66
1/1
✓ Branch 1 taken 2 times.
46 .def_prop_ro("height", [](const Source& source) { return source.Grid().height; })
67
1/1
✓ Branch 2 taken 4 times.
48 .def_prop_ro("transform", [](const Source& source) { return source.Grid().transform; });
68 44 }
69
70 44 void BindSourceTypes(nb::module_& m) {
71 44 nb::class_<FlatSource, Source>(m, "FlatSource",
72 "A level datum at `elevation` over a `width` x `height` grid of "
73 "`pixel_size` metres centred on `centre`.")
74 .def(
75 "__init__",
76 184 [](FlatSource* self, std::size_t width, std::size_t height, double pixelSize,
77 float elevation, const Vec2d& centre, const Surface& surface) {
78
1/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 new (self)
79
1/1
✓ Branch 3 taken 139 times.
141 FlatSource(width, height, pixelSize / surface.UnitLength(), elevation, centre);
80 139 },
81
5/5
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
✓ Branch 10 taken 44 times.
✓ Branch 13 taken 44 times.
176 "width"_a, "height"_a, "pixel_size"_a, "elevation"_a = 0.0f, "centre"_a = Vec2d{},
82
1/1
✓ Branch 2 taken 44 times.
88 "surface"_a = Surface{},
83 "On a sphere `centre` is (longitude, latitude) in degrees and `pixel_size` stays in "
84 "metres, converted to degrees along a meridian.");
85 44 nb::class_<GeoTiffSource, Source>(
86 m, "GeoTiffSource",
87 "Heights from band `band` of a GeoTIFF, optionally low-passed by a Gaussian of `low_pass` "
88 "metres, resampled to `pixel_size` metres and scaled (height * scale + offset).")
89 .def(
90 "__init__",
91 51 [](GeoTiffSource* self, const std::filesystem::path& path, std::size_t band,
92 std::optional<double> pixelSize, ResampleMethod resampling,
93 std::optional<double> scale, std::optional<double> offset, double lowPass) {
94
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ new (self)
95
2/2
✓ Branch 2 taken 7 times.
✓ Branch 5 taken 7 times.
7 GeoTiffSource(path, {band, pixelSize, resampling, scale, offset, lowPass});
96 7 },
97
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
176 "path"_a, "band"_a = 0, "pixel_size"_a = nb::none(),
98
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
176 "resampling"_a = ResampleMethod::Cubic, "scale"_a = nb::none(), "offset"_a = nb::none(),
99
1/1
✓ Branch 1 taken 44 times.
88 "low_pass"_a = 0.0);
100 ✗ nb::class_<SourceChain, Source>(
101 m, "SourceChain",
102 "Several sources tried in order, each filling where those before it have no data; the "
103 "grid is the first's.")
104 .def(
105 "__init__",
106 46 [](SourceChain* self, const std::vector<std::shared_ptr<Source>>& sources) {
107
2/4
✓ Branch 4 taken 2 times.
✓ Branch 7 taken 2 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
4 new (self) SourceChain({sources.begin(), sources.end()});
108 2 },
109
1/1
✓ Branch 1 taken 44 times.
88 "sources"_a);
110 44 }
111
112 /// A field written in Python: `value_at(x, y)` and `max()` are required.
113 class PyField : public SurfaceField {
114 public:
115 NB_TRAMPOLINE(SurfaceField);
116
117 ✗ double ValueAt(double x, double y) const override {
118
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ NB_OVERRIDE_PURE_NAME("value_at", ValueAt, x, y);
119 }
120
121 ✗ double Max() const override {
122
0/2
✗ Branch 2 not taken.
✗ Branch 5 not taken.
✗ NB_OVERRIDE_PURE_NAME("max", Max);
123 }
124 };
125
126 44 void BindFields(nb::module_& m) {
127 88 nb::class_<SurfaceField, PyField>(
128 m, "SurfaceField",
129 "A scalar quantity over a surface, read at surface coordinates (metres on a plane, "
130 "longitude and latitude in degrees on a sphere), such as a density map or a catalog's "
131 "completeness limit. Subclass it and implement `value_at(x, y)` (NaN where it has no "
132 "value) and `max()` for a field of your own.")
133 44 .def(nb::init<>())
134
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 .def("value_at", &SurfaceField::ValueAt, "x"_a, "y"_a)
135 88 .def("max", &SurfaceField::Max);
136 44 nb::class_<ConstantField, SurfaceField>(m, "ConstantField", "The same `value` everywhere.")
137
1/1
✓ Branch 1 taken 44 times.
88 .def(nb::init<double>(), "value"_a);
138 auto options = nb::class_<RasterFieldOptions>(
139 m, "RasterFieldOptions",
140 "How a raster field is read: `method` (nearest or bilinear), `scale` multiplying every "
141 "cell, the `fallback` where the raster has no value (NaN for none), and whether x is a "
142 44 "`periodic_longitude`.");
143
1/1
✓ Branch 1 taken 44 times.
44 BindKwargsInit<RasterFieldOptions>(options);
144 44 options.def_rw("method", &RasterFieldOptions::method)
145 44 .def_rw("scale", &RasterFieldOptions::scale)
146 44 .def_rw("fallback", &RasterFieldOptions::fallback)
147 .def_rw("periodic_longitude", &RasterFieldOptions::periodicLongitude);
148
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_raster_field_options", &ValidateRasterFieldOptions, "options"_a,
149 "Raise ValueError if the options are unusable.");
150 ✗ nb::class_<RasterField, SurfaceField>(
151 m, "RasterField", "A field interpolated from a raster in the surface's coordinates.")
152
1/1
✓ Branch 1 taken 44 times.
44 .def(nb::init<Raster<float>, RasterFieldOptions>(), "raster"_a,
153
1/1
✓ Branch 2 taken 44 times.
88 "options"_a = RasterFieldOptions{})
154 44 .def_prop_ro("values", &RasterField::Values)
155 88 .def_prop_ro("options", &RasterField::Options);
156
3/3
✓ Branch 2 taken 44 times.
✓ Branch 5 taken 44 times.
✓ Branch 8 taken 44 times.
88 m.def("read_raster_field", &ReadRasterField, "path"_a, "options"_a = RasterFieldOptions{},
157
1/1
✓ Branch 1 taken 44 times.
88 "band"_a = 0, "A field read from one band of a GeoTIFF in the surface's coordinates.");
158 44 }
159
160 44 void BindSyntheticTerrain(nb::module_& m) {
161 44 nb::class_<SyntheticTerrainParams>(m, "SyntheticTerrainParams",
162 "A stack of 1 to 8 noise layers. `layers` is copied on "
163 "access; assign a new list to change it.")
164 44 .def(
165 "__init__",
166 ✗ [](SyntheticTerrainParams* params, std::optional<std::vector<NoiseLayer>> layers) {
167
1/3
✓ Branch 2 taken 12 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
12 new (params) SyntheticTerrainParams{};
168
2/2
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 5 times.
12 if (layers) {
169 7 params->layers = *layers;
170 }
171 12 },
172
1/1
✓ Branch 1 taken 44 times.
88 "layers"_a = nb::none())
173 44 .def_rw("layers", &SyntheticTerrainParams::layers)
174
1/1
✓ Branch 2 taken 44 times.
44 .def(nb::self == nb::self);
175
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 m.def("validate_synthetic_terrain", &ValidateSyntheticTerrain, "params"_a,
176 "Raise ValueError if the terrain parameters are unusable.");
177
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("fill_synthetic_terrain", &FillSyntheticTerrain, "raster"_a, "params"_a,
178 ✗ nb::call_guard<nb::gil_scoped_release>(),
179 "Set every cell of `raster` to the synthetic terrain height at its centre.");
180
3/3
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
✓ Branch 7 taken 44 times.
44 m.def("generate_synthetic_terrain", &GenerateSyntheticTerrain, "width"_a, "height"_a,
181
2/2
✓ Branch 1 taken 44 times.
✓ Branch 4 taken 44 times.
44 "pixel_size"_a, "params"_a, nb::call_guard<nb::gil_scoped_release>(),
182 "Generate a north-up synthetic terrain centred on the world origin, with square pixels "
183 "of `pixel_size` metres.");
184 44 }
185
186 } // namespace
187
188 44 void BindSources(nb::module_& m) {
189 44 BindSourceBase(m);
190 44 BindSourceTypes(m);
191 44 BindFields(m);
192 44 BindSyntheticTerrain(m);
193 44 }
194
195 } // namespace terraingen::python
196