diff --git a/.gitignore b/.gitignore index bae5fad..855e362 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ _dist/ _req_packages/ map_tile_cache/ map_tile_cache_ge/ -geoelevation_dem_cache/ \ No newline at end of file +geoelevation_dem_cache/ +build/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 6e38950..6566a30 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,4 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { @@ -9,6 +6,22 @@ "type": "debugpy", "request": "launch", "module": "geoelevation" - } + }, + { + "name": "(Windows) Launch C++ CLI", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/build/elevation_cli.exe", + "args": [ + // Argomenti a riga di comando per il test + "${workspaceFolder}/geoelevation_dem_cache/hgt_tiles", + "46.1705234", + "9.403258" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "console": "integratedTerminal" // Mostra l'output nel terminale integrato + }, ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1427d91 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cmake.sourceDirectory": "C:/src/____GitProjects/GeoElevation/source_c", + "files.associations": { + "string": "cpp" + } +} \ No newline at end of file diff --git a/source_c/CMakeLists.txt b/source_c/CMakeLists.txt new file mode 100644 index 0000000..8b61aa9 --- /dev/null +++ b/source_c/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.10) +project(GeoElevationCpp) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(elevation_cli main.cpp ElevationReader.cpp) \ No newline at end of file diff --git a/source_c/ElevationReader.cpp b/source_c/ElevationReader.cpp new file mode 100644 index 0000000..3f9ba5a --- /dev/null +++ b/source_c/ElevationReader.cpp @@ -0,0 +1,134 @@ +#include "ElevationReader.h" +#include +#include +#include +#include +#include +#include + +// The value used within HGT files to represent "no data" points (e.g., water bodies). +// This corresponds to the minimum possible value for a signed 16-bit integer. +constexpr int16_t HGT_NO_DATA_VALUE = std::numeric_limits::min(); // -32768 + +ElevationReader::ElevationReader(const std::string& hgt_directory) + : hgt_dir_path_(hgt_directory) {} + +double ElevationReader::get_elevation(double latitude, double longitude) { + // Validate input coordinates to be within the standard WGS 84 range. + if (latitude < -90.0 || latitude >= 90.0 || longitude < -180.0 || longitude >= 180.0) { + return USER_NO_DATA_VALUE; + } + + // Determine which tile file is needed for the given coordinates. + std::string basename = get_tile_basename(latitude, longitude); + + // Load the tile from disk if it's not already in the cache. + if (!ensure_tile_is_loaded(basename)) { + // This occurs if the file doesn't exist or is invalid (e.g., wrong size). + std::cerr << "Warning: Tile file for " << basename << ".hgt not found or is invalid." << std::endl; + return USER_NO_DATA_VALUE; + } + + // Retrieve the tile data from the cache. `at()` is used for safe access. + const auto& data = tile_cache_.at(basename); + + // Calculate the fractional part of the coordinates to find the position within the 1x1 degree tile. + double lat_fractional = latitude - floor(latitude); + double lon_fractional = longitude - floor(longitude); + + // IMPORTANT: HGT data is stored in rows from North to South (top to bottom). + // A higher latitude corresponds to a lower row index. So, we invert the latitude logic. + // Example: For N45 tile (45-46 deg), lat 45.99 is near row 0; lat 45.01 is near row 3600. + int row = static_cast(round((1.0 - lat_fractional) * (HGT_GRID_DIMENSION - 1))); + int col = static_cast(round(lon_fractional * (HGT_GRID_DIMENSION - 1))); + + // Calculate the 1D index into the data vector. + size_t index = static_cast(row) * HGT_GRID_DIMENSION + col; + + // Safety check to prevent out-of-bounds access. + if (index >= data.size()) { + return USER_NO_DATA_VALUE; + } + + int16_t elevation = data[index]; + + // Check if the point corresponds to a "no data" value within the file. + if (elevation == HGT_NO_DATA_VALUE) { + return USER_NO_DATA_VALUE; + } + + return static_cast(elevation); +} + +std::string ElevationReader::get_tile_basename(double latitude, double longitude) const { + // The HGT filename convention is based on the integer coordinates of the bottom-left (SW) corner. + int lat_int = static_cast(floor(latitude)); + int lon_int = static_cast(floor(longitude)); + + char lat_char = (lat_int >= 0) ? 'N' : 'S'; + char lon_char = (lon_int >= 0) ? 'E' : 'W'; + + // Use a stringstream for safe and clean string formatting. + std::stringstream ss; + ss << lat_char << std::setfill('0') << std::setw(2) << std::abs(lat_int) + << lon_char << std::setfill('0') << std::setw(3) << std::abs(lon_int); + + return ss.str(); +} + +bool ElevationReader::ensure_tile_is_loaded(const std::string& tile_basename) { + // 1. Check if the tile is already in our memory cache to avoid disk I/O. + if (tile_cache_.count(tile_basename)) { + return true; + } + + // 2. Construct the full path to the .hgt file. + std::string file_path = hgt_dir_path_ + "/" + tile_basename + ".hgt"; + + std::ifstream file(file_path, std::ios::binary); + if (!file) { + return false; // File does not exist or cannot be opened. + } + + // 3. Validate the file size to ensure it's a complete 1-arc-second tile. + file.seekg(0, std::ios::end); + std::streampos file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + const std::streampos expected_size = HGT_GRID_DIMENSION * HGT_GRID_DIMENSION * sizeof(int16_t); + if (file_size != expected_size) { + std::cerr << "Warning: Invalid file size for " << file_path + << ". Expected " << expected_size << " bytes, but got " << file_size << " bytes." << std::endl; + return false; + } + + // 4. Read the entire file into a vector of 16-bit integers. + std::vector tile_data(HGT_GRID_DIMENSION * HGT_GRID_DIMENSION); + file.read(reinterpret_cast(tile_data.data()), expected_size); + + if (!file) { + std::cerr << "Error: Failed to read all data from " << file_path << std::endl; + return false; + } + + // 5. CRITICAL STEP: Convert every value from Big-Endian to the host's native endianness. + for (auto& val : tile_data) { + val = byteswap(val); + } + + // 6. Store the loaded and processed data in the cache using std::move for efficiency. + tile_cache_[tile_basename] = std::move(tile_data); + + return true; +} + +int16_t ElevationReader::byteswap(int16_t value) const { + // Cast to unsigned to prevent issues with sign extension during bit shifts on negative numbers. + uint16_t u_value = static_cast(value); + + // Example: 0x1234 (Big Endian) becomes 0x3412 (Little Endian) + // - (u_value & 0xFF00) >> 8 -> (0x1200) >> 8 -> 0x0012 + // - (u_value & 0x00FF) << 8 -> (0x0034) << 8 -> 0x3400 + // - 0x0012 | 0x3400 = 0x3412 + return static_cast(((u_value & 0xFF00) >> 8) | ((u_value & 0x00FF) << 8)); +} \ No newline at end of file diff --git a/source_c/ElevationReader.h b/source_c/ElevationReader.h new file mode 100644 index 0000000..655cb5b --- /dev/null +++ b/source_c/ElevationReader.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + + +// A constant for the expected grid dimension of 1-arc-second NASADEM HGT files. +constexpr int HGT_GRID_DIMENSION = 3601; + +// The special value returned to the user when elevation data is not available. +constexpr double USER_NO_DATA_VALUE = -999.0; + +/** + * @class ElevationReader + * @brief Reads elevation data from .hgt files downloaded by the GeoElevation tool. + * + * This class manages the loading, caching, and querying of 1-arc-second NASADEM .hgt files. + * It is designed to find the correct tile for a given coordinate, handle the binary + * file reading, and correctly interpret the big-endian data format used in HGT files. + */ +class ElevationReader { +public: + /** + * @brief Constructs an ElevationReader. + * @param hgt_directory The path to the directory containing the .hgt tile files. + */ + explicit ElevationReader(const std::string& hgt_directory); + + /** + * @brief Gets the elevation for a specific geographic coordinate. + * + * This is the main public method of the class. It orchestrates finding the correct + * tile, loading it if necessary, and calculating the elevation. + * + * @param latitude The latitude in decimal degrees (e.g., 45.072). + * @param longitude The longitude in decimal degrees (e.g., 7.685). + * @return The elevation in meters as a double. Returns USER_NO_DATA_VALUE (-999.0) + * if the corresponding .hgt tile is not found, coordinates are invalid, + * or if the point corresponds to a "no data" value in the file. + */ + double get_elevation(double latitude, double longitude); + +private: + /** + * @brief Generates the base filename (e.g., "N45E007") for a given coordinate. + * @param latitude The latitude of the point. + * @param longitude The longitude of the point. + * @return The standard base filename for the tile. + */ + std::string get_tile_basename(double latitude, double longitude) const; + + /** + * @brief Loads a tile from disk into the cache if it's not already present. + * + * This method implements a lazy-loading strategy. A tile is only read from disk + * the first time it is needed. Subsequent requests for the same tile will use + * the in-memory cache. + * + * @param tile_basename The base name of the tile to load (e.g., "N45E007"). + * @return True if the tile was loaded successfully or was already in the cache, false otherwise. + */ + bool ensure_tile_is_loaded(const std::string& tile_basename); + + /** + * @brief Converts a 16-bit integer from big-endian to the host system's endianness. + * + * HGT files store data in big-endian (network byte order), while most modern CPUs + * (x86/x64) are little-endian. This byte swap is a critical step to correctly + * interpret the elevation values. + * + * @param value The big-endian short value read from the file. + * @return The value converted to the host system's native byte order. + */ + int16_t byteswap(int16_t value) const; + + // The path to the directory where .hgt files are stored. + std::string hgt_dir_path_; + + // In-memory cache for tile data to avoid repeated, slow file reads from disk. + // The key is the tile basename (e.g., "N45E007"), the value is the elevation data. + std::map> tile_cache_; +}; \ No newline at end of file diff --git a/source_c/elevation_cli.exe b/source_c/elevation_cli.exe new file mode 100644 index 0000000..617f12e Binary files /dev/null and b/source_c/elevation_cli.exe differ diff --git a/source_c/main.cpp b/source_c/main.cpp new file mode 100644 index 0000000..26e8c85 --- /dev/null +++ b/source_c/main.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include "ElevationReader.h" +void print_usage() +{ + std::cerr << "Usage: elevation_cli " << std::endl; + std::cerr << "Example: elevation_cli ../geoelevation_dem_cache/hgt_tiles 45.07 7.68" << std::endl; +} + +int main(int argc, char* argv[]) +{ + if (argc != 4) + { + print_usage(); + return 1; + } + + std::string hgt_dir; + double lat, lon; + + try + { + hgt_dir = argv[1]; + lat = std::stod(argv[2]); + lon = std::stod(argv[3]); + } catch (const std::invalid_argument& e) + { + std::cerr << "Error: Invalid latitude or longitude format." << std::endl; + print_usage(); + return 1; + } catch (const std::out_of_range& e) + { + std::cerr << "Error: Latitude or longitude value is out of range." << std::endl; + print_usage(); + return 1; + } + + try + { + ElevationReader reader(hgt_dir); + double elevation = reader.get_elevation(lat, lon); + std::cout << "Querying for Latitude=" << lat << ", Longitude=" << lon << std::endl; + if (elevation == USER_NO_DATA_VALUE) + { + std::cout << "Result: Elevation data not available for this location." << std::endl; + } + else + { + std::cout << "Result: Elevation is " << elevation << " meters." << std::endl; + } + } catch (const std::exception& e) + { + std::cerr << "An unexpected error occurred: " << e.what() << std::endl; + return 1; + } + return 0; +} \ No newline at end of file