Skip to content

File blaze_output_loader.hpp

File List > gui > blaze_output_loader.hpp

Go to the documentation of this file

#pragma once

#include <algorithm>
#include <functional>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <vector>

#include "gui/layer.hpp"
#include "utilities/filesystem.hpp"
#include "utilities/progress_tracker.hpp"

struct BlazeOutputSet {
  fs::path root;
  std::optional<fs::path> filled_dem;
  std::optional<fs::path> smooth_ground;
  std::optional<fs::path> ground;
  std::optional<fs::path> slope;
  std::optional<fs::path> fine_slope;
  std::optional<fs::path> final_img;
  std::optional<fs::path> contours;
  std::optional<fs::path> streams;
};

struct BlazeOutputDiscovery {
  BlazeOutputSet outputs;
  std::vector<fs::path> search_roots;
  std::vector<fs::path> locations_with_outputs;
};

namespace detail {

inline bool is_blaze_output_filename(const std::string& name) {
  static const std::vector<std::string> KNOWN{
      "filled_dem.tif", "smooth_ground.tif",     "ground.tif",
      "slope.tif",      "fine_slope.tif",        "final_img.tif",
      "contours.gpkg",  "trimmed_contours.gpkg", "streams.gpkg"};
  return std::find(KNOWN.begin(), KNOWN.end(), name) != KNOWN.end();
}

inline void collect_search_roots(const fs::path& directory, std::vector<fs::path>& roots) {
  auto add_root = [&](const fs::path& candidate) {
    if (!fs::is_directory(candidate)) {
      return;
    }
    if (std::find(roots.begin(), roots.end(), candidate) == roots.end()) {
      roots.push_back(candidate);
    }
  };

  add_root(directory);
  add_root(directory / "combined");

  if (!fs::is_directory(directory)) {
    return;
  }

  std::error_code ec;
  for (const fs::directory_entry& entry : fs::directory_iterator(directory, ec)) {
    if (ec || !entry.is_directory()) {
      continue;
    }
    const fs::path subdir = entry.path();
    add_root(subdir);
    add_root(subdir / "combined");
  }
}

inline std::optional<fs::path> find_in_root(const fs::path& root,
                                            const std::vector<std::string>& names) {
  for (const std::string& name : names) {
    const fs::path candidate = root / name;
    if (fs::exists(candidate)) {
      return candidate;
    }
  }
  return std::nullopt;
}

inline int blaze_output_score(const BlazeOutputSet& outputs) {
  int score = 0;
  if (outputs.filled_dem) {
    score += 4;
  }
  if (outputs.contours) {
    score += 3;
  }
  if (outputs.final_img) {
    score += 2;
  }
  if (outputs.slope) {
    score += 1;
  }
  return score;
}

inline BlazeOutputSet discover_in_root(const fs::path& root) {
  BlazeOutputSet outputs;
  outputs.root = root;

  outputs.filled_dem = find_in_root(root, {"filled_dem.tif"});
  outputs.smooth_ground = find_in_root(root, {"smooth_ground.tif"});
  outputs.ground = find_in_root(root, {"ground.tif"});
  if (!outputs.filled_dem) {
    outputs.filled_dem = outputs.smooth_ground;
  }
  if (!outputs.filled_dem) {
    outputs.filled_dem = outputs.ground;
  }
  outputs.slope = find_in_root(root, {"slope.tif"});
  outputs.fine_slope = find_in_root(root, {"fine_slope.tif"});
  outputs.final_img = find_in_root(root, {"final_img.tif"});
  outputs.contours = find_in_root(root, {"contours.gpkg", "trimmed_contours.gpkg"});
  outputs.streams = find_in_root(root, {"streams.gpkg"});

  return outputs;
}

inline bool has_importable_outputs(const BlazeOutputSet& outputs) {
  return outputs.filled_dem.has_value() || outputs.contours.has_value();
}

inline bool is_combined_root(const fs::path& root) { return root.filename() == "combined"; }

inline void collect_locations_with_outputs(const fs::path& directory,
                                           std::vector<fs::path>& locations) {
  if (!fs::is_directory(directory)) {
    return;
  }

  auto note_if_outputs = [&](const fs::path& root) {
    if (!fs::is_directory(root)) {
      return;
    }
    const BlazeOutputSet candidate = discover_in_root(root);
    if (has_importable_outputs(candidate) &&
        std::find(locations.begin(), locations.end(), root) == locations.end()) {
      locations.push_back(root);
    }
  };

  note_if_outputs(directory);

  std::error_code ec;
  for (const fs::directory_entry& entry : fs::directory_iterator(directory, ec)) {
    if (ec) {
      continue;
    }
    if (entry.is_directory()) {
      note_if_outputs(entry.path());
      note_if_outputs(entry.path() / "combined");
      continue;
    }
    if (entry.is_regular_file() && is_blaze_output_filename(entry.path().filename().string())) {
      if (std::find(locations.begin(), locations.end(), directory) == locations.end()) {
        locations.push_back(directory);
      }
    }
  }
}

}  // namespace detail

inline BlazeOutputDiscovery discover_blaze_output_with_info(const fs::path& directory) {
  BlazeOutputDiscovery discovery;
  detail::collect_search_roots(directory, discovery.search_roots);
  detail::collect_locations_with_outputs(directory, discovery.locations_with_outputs);

  std::vector<BlazeOutputSet> candidates;
  candidates.reserve(discovery.search_roots.size());
  for (const fs::path& root : discovery.search_roots) {
    BlazeOutputSet candidate = detail::discover_in_root(root);
    if (detail::has_importable_outputs(candidate)) {
      candidates.push_back(std::move(candidate));
    }
  }

  const auto pick_best = [](const std::vector<BlazeOutputSet>& options) -> const BlazeOutputSet* {
    const BlazeOutputSet* best = nullptr;
    int best_score = -1;
    for (const BlazeOutputSet& candidate : options) {
      const int score = detail::blaze_output_score(candidate);
      if (score > best_score) {
        best_score = score;
        best = &candidate;
      }
    }
    return best;
  };

  std::vector<BlazeOutputSet> combined_candidates;
  combined_candidates.reserve(candidates.size());
  for (const BlazeOutputSet& candidate : candidates) {
    if (detail::is_combined_root(candidate.root)) {
      combined_candidates.push_back(candidate);
    }
  }

  if (const BlazeOutputSet* best = pick_best(combined_candidates)) {
    discovery.outputs = *best;
    return discovery;
  }

  std::vector<BlazeOutputSet> direct_candidates;
  direct_candidates.reserve(candidates.size());
  for (const BlazeOutputSet& candidate : candidates) {
    if (candidate.root == directory) {
      direct_candidates.push_back(candidate);
    }
  }
  if (const BlazeOutputSet* best = pick_best(direct_candidates)) {
    discovery.outputs = *best;
    return discovery;
  }

  std::vector<BlazeOutputSet> tile_candidates;
  tile_candidates.reserve(candidates.size());
  for (const BlazeOutputSet& candidate : candidates) {
    if (candidate.root.parent_path() == directory && !detail::is_combined_root(candidate.root)) {
      tile_candidates.push_back(candidate);
    }
  }
  if (tile_candidates.size() == 1) {
    discovery.outputs = tile_candidates.front();
    return discovery;
  }
  if (tile_candidates.size() > 1) {
    discovery.locations_with_outputs.clear();
    for (const BlazeOutputSet& candidate : tile_candidates) {
      discovery.locations_with_outputs.push_back(candidate.root);
    }
    discovery.outputs.root = directory;
    return discovery;
  }

  if (const BlazeOutputSet* best = pick_best(candidates)) {
    discovery.outputs = *best;
  } else {
    discovery.outputs.root = directory;
  }
  return discovery;
}

inline BlazeOutputSet discover_blaze_output(const fs::path& directory) {
  return discover_blaze_output_with_info(directory).outputs;
}

inline std::string format_blaze_output_discovery_error(const fs::path& directory,
                                                       const BlazeOutputDiscovery& discovery) {
  std::ostringstream message;
  message << "Could not find blaze outputs in:\n"
          << directory.string() << "\n\nExpected at least one of:\n"
          << "  filled_dem.tif (or smooth_ground.tif / ground.tif)\n"
          << "  contours.gpkg (or trimmed_contours.gpkg)\n"
          << "in the selected folder, a combined/ subfolder, or a tile subfolder.\n";

  if (!discovery.locations_with_outputs.empty()) {
    message << "\nFound partial outputs in:\n";
    for (const fs::path& location : discovery.locations_with_outputs) {
      message << "  " << location.string() << "\n";
    }
    if (discovery.locations_with_outputs.size() > 1 &&
        !detail::has_importable_outputs(discovery.outputs)) {
      message << "\nMultiple tile folders contain blaze outputs. Run the Combine step in Blaze, "
                 "or select one tile folder to import.";
    }
  } else if (fs::is_directory(directory)) {
    message << "\nNo blaze output files were found under this folder.";
    std::vector<std::string> child_dirs;
    std::error_code ec;
    for (const fs::directory_entry& entry : fs::directory_iterator(directory, ec)) {
      if (!ec && entry.is_directory()) {
        child_dirs.push_back(entry.path().filename().string());
      }
    }
    if (!child_dirs.empty()) {
      message << " Subfolders present: ";
      for (size_t i = 0; i < child_dirs.size(); ++i) {
        if (i > 0) {
          message << ", ";
        }
        message << child_dirs[i];
        if (i >= 9 && child_dirs.size() > 10) {
          message << ", ... (" << child_dirs.size() << " total)";
          break;
        }
      }
      message << ".";
    }
  } else {
    message << "\nThe selected path is not a directory.";
  }

  return message.str();
}

inline void append_flat_grid_dem_layers(
    std::vector<std::unique_ptr<Layer>>& layers, const BlazeOutputSet& outputs,
    const std::function<AsyncProgressTracker()>& progress_factory, const std::string& target_crs) {
  std::set<std::string> seen_paths;
  auto append = [&](const std::optional<fs::path>& dem_path) {
    if (!dem_path) {
      return;
    }
    const std::string key = dem_path->lexically_normal().string();
    if (!seen_paths.insert(key).second) {
      return;
    }
    auto layer =
        std::make_unique<DemLayer>(*dem_path, progress_factory(), std::nullopt, target_crs, true);
    layer->set_visible(false);
    layers.push_back(std::move(layer));
  };
  append(outputs.ground);
  append(outputs.smooth_ground);
  append(outputs.filled_dem);
}

// Single source of truth for viewer layers built from discovered blaze outputs.
// GUI import and any other callers should use this instead of duplicating layer setup.
inline std::vector<std::unique_ptr<Layer>> load_blaze_outputs(
    const BlazeOutputSet& outputs, const std::function<AsyncProgressTracker()>& progress_factory,
    const std::function<std::string()>& reference_crs_factory = [] { return std::string{}; }) {
  std::vector<std::unique_ptr<Layer>> layers;
  const std::string target_crs = reference_crs_factory();

  if (outputs.filled_dem) {
    if (outputs.final_img) {
      layers.push_back(std::make_unique<TexturedDemLayer>(*outputs.filled_dem, *outputs.final_img,
                                                          progress_factory(), target_crs));
    } else {
      layers.push_back(std::make_unique<DemLayer>(*outputs.filled_dem, progress_factory(),
                                                  std::nullopt, target_crs));
    }

    if (outputs.slope) {
      const fs::path& slope_dem =
          outputs.smooth_ground ? *outputs.smooth_ground : *outputs.filled_dem;
      auto slope_layer =
          std::make_unique<SlopeLayer>(slope_dem, *outputs.slope, progress_factory(), target_crs);
      if (outputs.final_img) {
        slope_layer->set_visible(false);
      }
      layers.push_back(std::move(slope_layer));
    }
  }

  if (outputs.fine_slope) {
    const fs::path ground_dem =
        outputs.ground ? *outputs.ground : outputs.fine_slope->parent_path() / "ground.tif";
    if (fs::exists(ground_dem)) {
      auto fine_slope_layer = std::make_unique<SlopeLayer>(ground_dem, *outputs.fine_slope,
                                                           progress_factory(), target_crs);
      if (outputs.final_img) {
        fine_slope_layer->set_visible(false);
      }
      layers.push_back(std::move(fine_slope_layer));
    }
  }

  append_flat_grid_dem_layers(layers, outputs, progress_factory, target_crs);

  if (outputs.contours) {
    layers.push_back(
        std::make_unique<ContourLayer>(*outputs.contours, progress_factory(), target_crs));
  }

  return layers;
}