Skip to content

File config_input.cpp

File List > config_input > config_input.cpp

Go to the documentation of this file

#include "config_input.hpp"

#include <fstream>

#include "assert/assert.hpp"

#define JSON_DIAGNOSTICS 1
#ifdef _MSC_VER
#pragma warning(push, 0)
#endif
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Warray-bounds"
#include <nlohmann/json.hpp>
#pragma GCC diagnostic pop
#ifdef _MSC_VER
#pragma warning(pop)
#endif

using json = nlohmann::json;

#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;                                                                               \
  }

namespace nlohmann {

namespace {

template <typename T>
T json_number_or(const json& j, const char* key, T default_value) {
  if (!j.contains(key) || j[key].is_null()) {
    return default_value;
  }
  return j[key].get<T>();
}

}  // namespace

template <>
struct adl_serializer<GridConfig> {
  static GridConfig from_json(const json& j) {
    // Defaults for fresh configs.
    constexpr double DEFAULT_BIN_RES = 0.5;
    constexpr unsigned int DEFAULT_DOWNSAMPLE = 3u;
    constexpr double DEFAULT_VEG_RES = 3.0;
    constexpr double DEFAULT_CONTOUR_RES = 9.0;

    const double bin_res = json_number_or(j, "bin_resolution", DEFAULT_BIN_RES);
    const unsigned int downsample = json_number_or(j, "downsample_factor", DEFAULT_DOWNSAMPLE);

    // Vegetation / contour resolutions are independent of bin/downsample.
    // For backward compatibility, configs that omit these but provide the
    // legacy bin_resolution / downsample_factor keys get the legacy
    // behaviour: vegetation at bin_resolution, contour DEM at the smooth-
    // ground resolution (bin_resolution * downsample_factor). Configs that
    // omit everything get the new defaults.
    const bool has_legacy_only = !j.contains("vegetation_grid_resolution") &&
                                 !j.contains("contour_dem_resolution") &&
                                 (j.contains("bin_resolution") || j.contains("downsample_factor"));

    double veg_res = DEFAULT_VEG_RES;
    double contour_res = DEFAULT_CONTOUR_RES;
    if (has_legacy_only) {
      veg_res = bin_res;
      contour_res = bin_res * static_cast<double>(downsample);
    }
    veg_res = json_number_or(j, "vegetation_grid_resolution", veg_res);
    contour_res = json_number_or(j, "contour_dem_resolution", contour_res);

    bool export_fine_slope = j.value("export_fine_slope", true);

    return GridConfig{bin_res, downsample, veg_res, contour_res, export_fine_slope};
  }

  static void to_json(json& j, GridConfig gc) {
    j["bin_resolution"] = gc.bin_resolution;
    j["downsample_factor"] = gc.downsample_factor;
    j["vegetation_grid_resolution"] = gc.vegetation_grid_resolution;
    j["contour_dem_resolution"] = gc.contour_dem_resolution;
    j["export_fine_slope"] = gc.export_fine_slope;
  }
};

template <>
struct adl_serializer<GroundConfig> {
  static GroundConfig from_json(const json& j) {
    return GroundConfig{static_cast<int>(json_number_or(j, "min_ground_intensity", 100)),
                        static_cast<int>(json_number_or(j, "max_ground_intensity", 1000)),
                        j.value("use_only_ground_class", true),
                        json_number_or(j, "outlier_threshold_m", 0.0)};
  }

  static void to_json(json& j, GroundConfig gc) {
    j["min_ground_intensity"] = gc.min_ground_intensity;
    j["max_ground_intensity"] = gc.max_ground_intensity;
    if (!gc.use_only_ground_class) {
      j["use_only_ground_class"] = gc.use_only_ground_class;
    }
    if (gc.outlier_threshold_m > 0.0) {
      j["outlier_threshold_m"] = gc.outlier_threshold_m;
    }
  }
};

template <>
struct adl_serializer<RGBColor> {
  static RGBColor from_json(const json& j) { return RGBColor(j[0], j[1], j[2], j[3]); }

  static void to_json(json& j, RGBColor c) {
    j = {c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha()};
  }
};

template <>
struct adl_serializer<CMYKColor> {
  static CMYKColor from_json(const json& j) { return CMYKColor(j[0], j[1], j[2], j[3]); }

  static void to_json(json& j, CMYKColor c) {
    j = {c.getCyan(), c.getMagenta(), c.getYellow(), c.getBlack()};
  }
};

template <>
struct adl_serializer<ColorVariant> {
  static ColorVariant from_json(const json& j) {
    if (j.is_string()) {
      return COLOR_MAP.at(j.get<std::string>());
    } else {
      const auto& color = j.items().begin();
      if (color.key() == "rgb") {
        return color.value().get<RGBColor>();
      } else {
        Assert(color.key() == "cmyk", "Invalid color type");
        return color.value().get<CMYKColor>();
      }
    }
  }

  static void to_json(json& j, ColorVariant cv) {
    if (std::holds_alternative<RGBColor>(cv)) {
      j = json({{"rgb", std::get<RGBColor>(cv)}});
    } else {
      j = json({{"cmyk", std::get<CMYKColor>(cv)}});
    }
  }
};

template <>
struct adl_serializer<ContourConfig> {
  static ContourConfig from_json(const json& j) {
    return ContourConfig{j.value("interval", 1.0), j.value("min_points", 5u),
                         j.value("color", json("brown")).get<ColorVariant>(),
                         j.value("width", 0.14)};
  }

  static void to_json(json& j, ContourConfig cc) {
    j["interval"] = cc.interval;
    j["min_points"] = cc.min_points;
    j["color"] = cc.color;
    j["width"] = cc.width;
  }
};

template <>
struct adl_serializer<BlockingThresholdColorPair> {
  static BlockingThresholdColorPair from_json(const json& j) {
    return BlockingThresholdColorPair{
        json_number_or(j, "blocking_threshold", 0.1),
        j.value("color", json("white")).get<ColorVariant>(), j.value("layer", std::string{}),
        json_number_or(j, "min_area_m2", 0.0), json_number_or(j, "min_hole_area_m2", 0.0)};
  }

  static void to_json(json& j, BlockingThresholdColorPair btc) {
    j["blocking_threshold"] = btc.blocking_threshold;
    j["color"] = btc.color;
    if (!btc.layer.empty()) {
      j["layer"] = btc.layer;
    }
    if (btc.min_area_m2 > 0) {
      j["min_area_m2"] = btc.min_area_m2;
    }
    if (btc.min_hole_area_m2 > 0) {
      j["min_hole_area_m2"] = btc.min_hole_area_m2;
    }
  }
};
template <>
struct adl_serializer<VegeHeightConfig> {
  static VegeHeightConfig from_json(const json& j) {
    return VegeHeightConfig{
        j.value("name", "Vegetation"), json_number_or(j, "min_height", 2.5),
        json_number_or(j, "max_height", 100.0),
        static_cast<int>(json_number_or(j, "smooth_radius", 3.0)),
        j.value("colors", json({})).get<std::vector<BlockingThresholdColorPair>>()};
  }

  static void to_json(json& j, VegeHeightConfig vhc) {
    j["name"] = vhc.name;
    j["min_height"] = vhc.min_height;
    j["max_height"] = vhc.max_height;
    if (vhc.smooth_radius != 3) {
      j["smooth_radius"] = vhc.smooth_radius;
    }
    j["colors"] = vhc.colors;
  }
};

template <>
struct adl_serializer<VegeConfig> {
  static VegeConfig from_json(const json& j) {
    return VegeConfig{j.value("background_color", json("white")).get<ColorVariant>(),
                      j.value("height_configs", json({})).get<std::vector<VegeHeightConfig>>()};
  }

  static void to_json(json& j, VegeConfig vc) {
    j["background_color"] = vc.background_color;
    j["height_configs"] = vc.height_configs;
  }
};
template <>
struct adl_serializer<RenderConfig> {
  static RenderConfig from_json(const json& j) {
    return RenderConfig{json_number_or(j, "scale", 10000.0), json_number_or(j, "dpi", 600.0)};
  }

  static void to_json(json& j, RenderConfig rc) {
    j["scale"] = rc.scale;
    j["dpi"] = rc.dpi;
  }
};

template <>
struct adl_serializer<WaterConfig> {
  static WaterConfig from_json(const json& j) {
    return WaterConfig{.catchment = j.value("catchment", 0.05),
                       .color = j.value("color", json("blue")),
                       .width = j.value("width", 0.18)};
  }

  static void to_json(json& j, WaterConfig cc) {
    j["catchment"] = cc.catchment;
    j["color"] = cc.color;
    j["width"] = cc.width;
  }
};

template <>
struct adl_serializer<WaterConfigs> {
  static WaterConfigs from_json(const json& j) {
    WaterConfigs water;
    if (!j.is_object()) {
      return water;
    }
    water.sink_min_area_m2 = json_number_or(j, "sink_min_area_m2", water.sink_min_area_m2);
    water.sink_depth_m = json_number_or(j, "sink_depth_m", water.sink_depth_m);
    if (j.contains("classified_overlay_color")) {
      water.classified_overlay_color = j.at("classified_overlay_color").get<ColorVariant>();
    }
    for (const auto& [key, value] : j.items()) {
      if (key == "sink_min_area_m2" || key == "sink_depth_m" || key == "classified_overlay_color") {
        continue;
      }
      water.configs.emplace(key, value.get<WaterConfig>());
    }
    return water;
  }

  static void to_json(json& j, WaterConfigs water) {
    j = water.configs;
    if (water.sink_min_area_m2 != 5000.0) {
      j["sink_min_area_m2"] = water.sink_min_area_m2;
    }
    if (water.sink_depth_m != 10.0) {
      j["sink_depth_m"] = water.sink_depth_m;
    }
    const CMYKColor default_overlay(100, 0, 0, 0);
    const CMYKColor overlay_cmyk = to_cmyk(water.classified_overlay_color);
    if (overlay_cmyk.getCyan() != default_overlay.getCyan() ||
        overlay_cmyk.getMagenta() != default_overlay.getMagenta() ||
        overlay_cmyk.getYellow() != default_overlay.getYellow() ||
        overlay_cmyk.getBlack() != default_overlay.getBlack()) {
      j["classified_overlay_color"] = water.classified_overlay_color;
    }
  }
};

template <>
struct adl_serializer<ContourConfigs> {
  static ContourConfigs from_json(const json& j) {
    return ContourConfigs(j.get<std::map<std::string, ContourConfig>>());
  }

  static void to_json(json& j, ContourConfigs cc) { j = cc.configs; }
};

template <>
struct adl_serializer<BuildingsConfig> {
  static BuildingsConfig from_json(const json& j) {
    return BuildingsConfig{j.value("color", json("black")).get<ColorVariant>()};
  }

  static void to_json(json& j, BuildingsConfig bc) { j["color"] = bc.color; }
};

SERIALIZE_ENUM_STRICT(ProcessingStep, {
                                          {ProcessingStep::Tiles, "tiles"},
                                          {ProcessingStep::Combine, "combine"},
                                      })

template <>
struct adl_serializer<Config> {
  static Config from_json(const json& j) {
    Config config;
    config.grid = j.value("grid", json({})).get<GridConfig>();
    config.ground = j.value("ground", json({})).get<GroundConfig>();
    config.contours = j.value("contours", json({})).get<ContourConfigs>();
    config.water = j.value("water", json({})).get<WaterConfigs>();
    config.vege = j.value("vege", json({})).get<VegeConfig>();
    config.render = j.value("render", json({})).get<RenderConfig>();
    config.buildings = j.value("buildings", json({})).get<BuildingsConfig>();
    config.las_files =
        j.value("las_files", json(std::vector<std::string>())).get<std::vector<fs::path>>();
    config.processing_steps = j.value("steps", json({"tiles"})).get<std::set<ProcessingStep>>();
    config.output_directory = j.value("output_directory", "out");
    config.border_width = j.value("border_width", 100.0);
    config.tile_size = j.value("tile_size", 0.0);
    config.override_crs = j.value("override_crs", std::string{});
    config.delete_tile_folders = j.value("delete_tile_folders", false);
    config.relative_path_to_config = "";
    return config;
  }

  static void to_json(json& j, const Config& gc) {
    j["grid"] = gc.grid;
    j["ground"] = gc.ground;
    j["contours"] = gc.contours;
    j["water"] = gc.water;
    j["vege"] = gc.vege;
    j["render"] = gc.render;
    j["colors"] = json({{"primitive", COLOR_MAP}});
    j["buildings"] = gc.buildings;
    j["las_files"] = gc.las_files;
    j["steps"] = gc.processing_steps;
    j["output_directory"] = gc.output_directory;
    j["border_width"] = gc.border_width;
    if (gc.tile_size > 0.0) {
      j["tile_size"] = gc.tile_size;
    }
    if (!gc.override_crs.empty()) {
      j["override_crs"] = gc.override_crs;
    }
    if (gc.delete_tile_folders) {
      j["delete_tile_folders"] = gc.delete_tile_folders;
    }
  }
};

}  // namespace nlohmann

std::ostream& operator<<(std::ostream& os, const Config& config) {
  os << "Config:\n";
  os << json(config).dump(4);
  return os;
}

void Config::write_to_file(const fs::path& filename) const {
  std::ofstream file(filename);
  if (!file.is_open()) {
    std::cerr << "Failed to open config file " << filename << std::endl;
    throw std::filesystem::filesystem_error("Failed to open config file " + filename.string(),
                                            std::make_error_code(std::errc::io_error));
  }
  json j = *this;
  j.erase("relative_path_to_config");
  file << j.dump(4);
  file.close();
}

Config Config::FromFile(const fs::path& filename) {
  std::ifstream file(filename);
  if (!file.is_open()) {
    std::cerr << "Failed to open config file " << filename << std::endl;
    throw std::filesystem::filesystem_error("Failed to open config file " + filename.string(),
                                            std::make_error_code(std::errc::io_error));
  }
  json j;
  try {
    j = json::parse(file, nullptr, true, true);
  } catch (const json::exception& e) {
    std::cerr << "JSON parsing error: " << e.what() << "\n";
    throw e;
  }
  file.close();
  if (j.contains("colors")) {
    if (j["colors"].contains("primitive")) {
      for (const auto& color : j["colors"]["primitive"].items()) {
        COLOR_MAP[color.key()] = color.value().get<ColorVariant>();
      }
    }
    if (j["colors"].contains("composite")) {
      for (const auto& color : j["colors"]["composite"].items()) {
        CMYKColor composite;
        for (const auto& component : color.value().items()) {
          composite =
              composite + to_cmyk(COLOR_MAP.at(component.key())) * component.value().get<double>();
        }
        COLOR_MAP[color.key()] = composite;
      }
    }
  }
  Config c = j.get<Config>();
  c.relative_path_to_config = filename.parent_path();
  return c;
}

std::ostream& operator<<(std::ostream& os, const ProcessingStep& step) { return os << json(step); }