File config_input.hpp
File List > config_input > config_input.hpp
Go to the documentation of this file
#pragma once
#include <cmath>
#include <iostream>
#include <optional>
#include <set>
#include <vector>
#include "isom/colors.hpp"
#include "utilities/filesystem.hpp"
#include "utilities/resources.hpp"
struct GridConfig {
// Resolution (m) at which raw LiDAR points are binned. This is the
// underlying working grid: ground / building / water / intensity rasters
// are produced at this resolution.
double bin_resolution = 0.5;
// Integer factor by which the binned grid is downsampled to obtain the
// smoothed ground DEM. The smoothed DEM is what slope, hill-shade and the
// smooth_ground.tif raster are computed from. Effective smooth-DEM
// resolution = bin_resolution * downsample_factor.
unsigned int downsample_factor = 3;
// Resolution (m) of the vegetation/canopy maps. Vegetation point counts
// are aggregated to this resolution; final vege_color and raw_vege rasters
// are written at this resolution. Should be >= bin_resolution.
double vegetation_grid_resolution = 3.0;
// Resolution (m) of the DEM used for contour generation, stream extraction,
// depression filling and contour orientation. Should be >= the smooth DEM
// resolution (bin_resolution * downsample_factor). Larger values produce
// smoother contours but lose fine terrain detail.
double contour_dem_resolution = 9.0;
// When true, write fine_slope.tif from ground.tif (bin resolution) in addition to
// slope.tif from smooth_ground.tif.
bool export_fine_slope = true;
// Integer factor used to aggregate the bin grid into the vegetation grid.
// Always >= 1.
unsigned int vegetation_aggregation_factor() const {
if (bin_resolution <= 0.0) return 1u;
long rounded = std::lround(vegetation_grid_resolution / bin_resolution);
if (rounded < 1) rounded = 1;
return static_cast<unsigned int>(rounded);
}
// Integer factor used to further downsample the smooth ground DEM into the
// contour DEM. Always >= 1.
unsigned int contour_downsample_factor() const {
const double smooth_res = bin_resolution * static_cast<double>(downsample_factor);
if (smooth_res <= 0.0) return 1u;
long rounded = std::lround(contour_dem_resolution / smooth_res);
if (rounded < 1) rounded = 1;
return static_cast<unsigned int>(rounded);
}
};
#define SERIALIZE_ENUM_STRICT(ENUM_TYPE, ...) \
template <typename BasicJsonType> \
inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) { \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[e](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool { \
return ej_pair.first == e; \
}); \
if (it == std::end(m)) throw std::invalid_argument("unknown enum value"); \
j = it->second; \
} \
template <typename BasicJsonType> \
inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) { \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[&j](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool { \
return ej_pair.second == j; \
}); \
if (it == std::end(m)) throw std::invalid_argument("unknown json value: " + std::string(j)); \
e = it->first; \
}
struct GroundConfig {
int min_ground_intensity = 100;
int max_ground_intensity = 1000;
// When true, only LAS points classified as ground are used for the DEM minimum.
bool use_only_ground_class = true;
// Vertical threshold (m) for spike removal. <= 0 uses bin_resolution (and scales with
// downsample_factor on the smooth ground DEM).
double outlier_threshold_m = 0.0;
};
struct ContourConfig {
double interval;
unsigned int min_points;
ColorVariant color;
double width;
};
struct BlockingThresholdColorPair {
double blocking_threshold;
ColorVariant color;
std::string
layer; // CRT layer name, e.g. "405_Forest". Empty → default from config name + threshold.
double min_area_m2 = 0; // minimum polygon area in m²; smaller polygons are dropped
double min_hole_area_m2 = 0; // minimum hole area in m²; smaller holes are filled
};
struct VegeHeightConfig {
std::string name;
double min_height = 2.5;
double max_height = 100.0;
// Low-pass filter radius in vegetation-grid cells applied before polygonization.
int smooth_radius = 3;
std::vector<BlockingThresholdColorPair> colors;
std::optional<ColorVariant> pick_from_blocked_proportion(double bp) const {
std::optional<ColorVariant> color;
for (const BlockingThresholdColorPair& btc : colors) {
if (bp >= btc.blocking_threshold) {
color = btc.color;
}
}
return color;
}
};
struct VegeConfig {
ColorVariant background_color;
std::vector<VegeHeightConfig> height_configs;
};
struct RenderConfig {
double scale = 10000.0;
double dpi = 600.0;
};
struct WaterConfig {
double catchment;
ColorVariant color;
double width;
};
struct WaterConfigs {
std::map<std::string, WaterConfig> configs;
// Color for LAS-classified water cells on the final map overlay.
ColorVariant classified_overlay_color = ColorVariant(CMYKColor(100, 0, 0, 0));
// Minimum depression area (m²) to treat as a sink when filling the DEM for streams.
double sink_min_area_m2 = 5000.0;
// Minimum depth (m) below the filled surface for a cell to belong to a sink region.
double sink_depth_m = 10.0;
const WaterConfig& config_from_catchment(double catchment) const {
const WaterConfig* max_valid_config = nullptr;
for (const auto& [_, config] : configs) {
if (config.catchment <= catchment &&
(max_valid_config == nullptr || config.catchment > max_valid_config->catchment))
max_valid_config = &config;
}
return *max_valid_config;
}
double minimum_catchment() const {
double min_catchment = std::numeric_limits<double>::max();
for (const auto& [_, config] : configs) {
min_catchment = std::min(config.catchment, min_catchment);
}
return min_catchment;
}
};
struct ContourConfigs {
std::map<std::string, ContourConfig> configs;
double min_interval;
static double minimum_interval(const std::map<std::string, ContourConfig>& configs) {
double min_interval = std::numeric_limits<double>::max();
for (const auto& [_, config] : configs) {
min_interval = std::min(min_interval, config.interval);
}
return min_interval;
}
ContourConfigs() : min_interval(std::numeric_limits<double>::max()) {}
explicit ContourConfigs(std::map<std::string, ContourConfig> in_configs)
: configs(std::move(in_configs)), min_interval(minimum_interval(configs)) {}
const ContourConfig& operator[](const std::string& key) const { return configs.at(key); }
const ContourConfig& pick_from_height(double height) const {
auto max_valid_interval = std::numeric_limits<double>::min();
const ContourConfig* config_to_return = nullptr;
for (const auto& [_, config] : configs) {
if (config.interval > max_valid_interval &&
std::fmod(std::abs(height), config.interval) < 1e-8) {
max_valid_interval = config.interval;
config_to_return = &config;
}
}
return *config_to_return;
}
std::string layer_name_from_height(double height) const {
auto max_valid_interval = std::numeric_limits<double>::min();
std::string layer_name = "Contour";
for (const auto& [name, config] : configs) {
if (config.interval > max_valid_interval &&
std::fmod(std::abs(height), config.interval) < 1e-8) {
if (name == "form_line" || name == "formline") {
layer_name = "103_Form_Line";
} else if (name == "index") {
layer_name = "102_Index_Contour";
} else if (name == "normal") {
layer_name = "101_Contour";
}
max_valid_interval = config.interval;
}
}
return layer_name;
}
};
struct BuildingsConfig {
ColorVariant color;
};
enum class ProcessingStep {
Tiles,
Combine,
};
std::ostream& operator<<(std::ostream& os, const ProcessingStep& step);
struct Config {
GridConfig grid;
GroundConfig ground;
ContourConfigs contours;
WaterConfigs water;
VegeConfig vege;
RenderConfig render;
BuildingsConfig buildings;
std::vector<fs::path> las_files;
std::set<ProcessingStep> processing_steps;
fs::path output_directory;
double border_width;
// When > 0, enables tiled processing mode. Blaze divides the union of all
// input files' extents (in the output CRS) into a regular grid of tiles
// with this side length (meters) and processes each tile independently,
// pulling and reprojecting points from every input LAS/LAZ file that
// overlaps the tile+border. Required when input files overlap each other
// or use different CRSes.
double tile_size = 0.0;
// Overrides the CRS of any input LAS/LAZ file regardless of whether the file
// embeds a projection. Useful for older ACT government datasets that ship
// with no projection at all, and for correcting files that embed the wrong
// CRS. When set, a warning is emitted if the embedded CRS (if any) disagrees
// with this value. Accepts anything OGRSpatialReference::SetFromUserInput
// understands, e.g. "EPSG:28355", a WKT string, or a proj.4 string. Empty
// string = use whatever the file embeds (error out if it embeds nothing).
std::string override_crs;
// When true, each per-tile output directory is removed after all tiles have
// been processed. Useful for freeing disk space once the Combine step has
// merged the individual tile outputs into the combined/ directory.
bool delete_tile_folders = false;
fs::path relative_path_to_config;
void set_output_directory(const fs::path& output_dir) { output_directory = output_dir; }
fs::path output_path() const {
if (output_directory.is_absolute()) {
return output_directory;
}
return relative_path_to_config / output_directory;
}
std::vector<fs::path> las_filepaths() const {
std::vector<fs::path> las_filepaths;
for (const fs::path& las_file : las_files) {
if (las_file.is_absolute()) {
las_filepaths.push_back(las_file);
} else {
las_filepaths.push_back(relative_path_to_config / las_file);
}
}
return las_filepaths;
}
std::vector<fs::path> get_las_files(const fs::path& las_path) const {
std::vector<fs::path> file_list;
fs::path actual_path = las_path.is_absolute() ? las_path : relative_path_to_config / las_path;
if (fs::exists(actual_path)) {
if (fs::is_directory(actual_path)) {
for (const fs::directory_entry& entry : fs::directory_iterator(actual_path)) {
if (entry.path().extension() == ".las" || entry.path().extension() == ".laz") {
file_list.push_back(entry.path());
}
}
} else if (actual_path.extension() == ".las" || actual_path.extension() == ".laz") {
file_list.push_back(actual_path);
}
}
return file_list;
}
static Config FromFile(const fs::path& filename);
void write_to_file(const fs::path& filename) const;
static Config Default() {
Config c = FromFile(AssetRetriever::get_asset("default_config.json"));
// The asset directory may be read-only (e.g. inside a mounted DMG or an
// installed .app bundle). Redirect any relative output path to a writable
// per-user data directory so the first run doesn't hit a read-only error.
if (!c.output_directory.is_absolute()) {
c.output_directory = LocalDataRetriever::get_local_data(c.output_directory);
}
return c;
}
Config& operator=(const Config& config) = default;
Config(const Config& config) = delete;
Config(Config&& config) = default;
Config() = default;
friend std::ostream& operator<<(std::ostream& os, const Config& config);
};