diff --git a/.gitmodules b/.gitmodules index 3498d407..5690c4f5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "external/cmake-conan"] path = external/cmake-conan url = https://github.com/conan-io/cmake-conan.git +[submodule "test/data"] + path = test/data + url = ../supercell-wx-test-data diff --git a/CMakeLists.txt b/CMakeLists.txt index 0453e1a0..22e3fbb9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) include(${PROJECT_SOURCE_DIR}/external/cmake-conan/conan.cmake) conan_cmake_configure(REQUIRES boost/1.76.0 + gtest/cci.20210126 openssl/1.1.1k vulkan-loader/1.2.172 GENERATORS cmake @@ -35,11 +36,13 @@ conan_basic_setup(TARGETS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") -set(SUPERCELL_WX_DIR ${PROJECT_SOURCE_DIR}) +set(SCWX_DIR ${PROJECT_SOURCE_DIR}) option(BUILD_DOCS "Build documentation" OFF) add_subdirectory(external) +add_subdirectory(wxdata) +add_subdirectory(test) if(BUILD_DOCS) add_subdirectory(docs) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..45c59dd0 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,2 @@ +cmake_minimum_required(VERSION 3.11) +include(test.cmake) diff --git a/test/data b/test/data new file mode 160000 index 00000000..40f68fe0 --- /dev/null +++ b/test/data @@ -0,0 +1 @@ +Subproject commit 40f68fe08977e5168679ce0419834312a6369205 diff --git a/test/source/scwx/util/rangebuf.test.cpp b/test/source/scwx/util/rangebuf.test.cpp new file mode 100644 index 00000000..7df4565a --- /dev/null +++ b/test/source/scwx/util/rangebuf.test.cpp @@ -0,0 +1,33 @@ +#include + +#include +#include +#include + +namespace scwx +{ +namespace util +{ + +TEST(rangebuf, smiles_mile) +{ + std::istringstream iss("smiles"); + iss.seekg(1); + util::rangebuf rb(iss.rdbuf(), 4); + + std::ostringstream oss; + + boost::iostreams::filtering_streambuf in; + in.push(rb); + + std::streamsize bytesCopied = boost::iostreams::copy(in, oss); + std::string substring {oss.str()}; + + EXPECT_EQ(substring, "mile"); + EXPECT_EQ(bytesCopied, 4); + EXPECT_EQ(iss.tellg(), 5); + EXPECT_EQ(iss.eof(), false); +} + +} // namespace util +} // namespace scwx diff --git a/test/source/scwx/wsr88d/rpg/ar2v_file.test.cpp b/test/source/scwx/wsr88d/rpg/ar2v_file.test.cpp new file mode 100644 index 00000000..f97c65cf --- /dev/null +++ b/test/source/scwx/wsr88d/rpg/ar2v_file.test.cpp @@ -0,0 +1,26 @@ +#include + +#include + +#include + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +TEST(ar2v_file, klsx) +{ + Ar2vFile file; + bool fileValid = + file.LoadFile(std::string(SCWX_TEST_DATA_DIR) + + "/nexrad/level2/Level2_KLSX_20210527_1757.ar2v"); + + EXPECT_EQ(fileValid, true); +} + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/test/source/scwx/wxtest.cpp b/test/source/scwx/wxtest.cpp new file mode 100644 index 00000000..34d03d6f --- /dev/null +++ b/test/source/scwx/wxtest.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test.cmake b/test/test.cmake new file mode 100644 index 00000000..1ceabc67 --- /dev/null +++ b/test/test.cmake @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.11) +project(scwx-test CXX) + +include(GoogleTest) + +find_package(Boost) +find_package(BZip2) +find_package(GTest) + +set(SRC_MAIN source/scwx/wxtest.cpp) +set(SRC_UTIL_TESTS source/scwx/util/rangebuf.test.cpp) +set(SRC_WSR88D_RPG_TESTS source/scwx/wsr88d/rpg/ar2v_file.test.cpp) + +add_executable(wxtest ${SRC_MAIN} + ${SRC_UTIL_TESTS} + ${SRC_WSR88D_RPG_TESTS}) + +source_group("Source Files\\main" FILES ${SRC_MAIN}) +source_group("Source Files\\util" FILES ${SRC_UTIL_TESTS}) +source_group("Source Files\\wsr88d\\rpg" FILES ${SRC_WSR88D_RPG_TESTS}) + +target_include_directories(wxtest PRIVATE ${GTest_INCLUDE_DIRS}) + +set_target_properties(wxtest PROPERTIES CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF) + +if (MSVC) + set_target_properties(wxtest PROPERTIES LINK_FLAGS "/ignore:4099") +endif() + +target_compile_definitions(wxtest PRIVATE SCWX_TEST_DATA_DIR="${SCWX_DIR}/test/data") + +gtest_discover_tests(wxtest) + +target_link_libraries(wxtest Boost::iostreams + Boost::log + BZip2::BZip2 + GTest::gtest + wxdata) + +if (WIN32) + target_link_libraries(wxtest Ws2_32) +endif() diff --git a/wxdata/CMakeLists.txt b/wxdata/CMakeLists.txt new file mode 100644 index 00000000..e4d5ce80 --- /dev/null +++ b/wxdata/CMakeLists.txt @@ -0,0 +1,2 @@ +cmake_minimum_required(VERSION 3.11) +include(wxdata.cmake) diff --git a/wxdata/include/scwx/util/rangebuf.hpp b/wxdata/include/scwx/util/rangebuf.hpp new file mode 100644 index 00000000..f85f4074 --- /dev/null +++ b/wxdata/include/scwx/util/rangebuf.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace scwx +{ +namespace util +{ + +class rangebuf : public std::streambuf +{ +public: + rangebuf(std::streambuf* sbuf, size_t size); + ~rangebuf() = default; + + rangebuf(const rangebuf&) = delete; + rangebuf& operator=(const rangebuf&) = delete; + + int underflow() override; + +private: + size_t size_; + std::streambuf* sbuf_; + std::vector data_; +}; + +} // namespace util +} // namespace scwx diff --git a/wxdata/include/scwx/wsr88d/rpg/ar2v_file.hpp b/wxdata/include/scwx/wsr88d/rpg/ar2v_file.hpp new file mode 100644 index 00000000..2f682ee5 --- /dev/null +++ b/wxdata/include/scwx/wsr88d/rpg/ar2v_file.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +class Ar2vFileImpl; + +/** + * @brief The Archive II file is specified in the Interface Control Document for + * the Archive II/User, Document Number 2620010H, published by the WSR-88D Radar + * Operations Center. + */ +class Ar2vFile +{ +public: + explicit Ar2vFile(); + ~Ar2vFile(); + + Ar2vFile(const Ar2vFile&) = delete; + Ar2vFile& operator=(const Ar2vFile&) = delete; + + Ar2vFile(Ar2vFile&&) noexcept; + Ar2vFile& operator=(Ar2vFile&&); + + bool LoadFile(const std::string& filename); + +private: + std::unique_ptr p; +}; + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/wxdata/source/scwx/util/rangebuf.cpp b/wxdata/source/scwx/util/rangebuf.cpp new file mode 100644 index 00000000..0975b604 --- /dev/null +++ b/wxdata/source/scwx/util/rangebuf.cpp @@ -0,0 +1,30 @@ +#include + +namespace scwx +{ +namespace util +{ + +rangebuf::rangebuf(std::streambuf* sbuf, size_t size) : + size_(size), sbuf_(sbuf), data_() +{ + size_t bufferSize = std::max(size, 4096u); + data_.reserve(bufferSize); +} + +int rangebuf::underflow() +{ + char* buf = data_.data(); + ptrdiff_t bufSize = data_.capacity(); + + // Read from underlying stream buffer into own buffer until needed characters + // have been consumed + size_t r(sbuf_->sgetn(buf, std::min(bufSize, size_))); + size_ -= r; + setg(buf, buf, buf + r); + return gptr() == egptr() ? traits_type::eof() : + traits_type::to_int_type(*gptr()); +} + +} // namespace util +} // namespace scwx diff --git a/wxdata/source/scwx/wsr88d/rpg/ar2v_file.cpp b/wxdata/source/scwx/wsr88d/rpg/ar2v_file.cpp new file mode 100644 index 00000000..382ed076 --- /dev/null +++ b/wxdata/source/scwx/wsr88d/rpg/ar2v_file.cpp @@ -0,0 +1,160 @@ +#include +#include + +#include +#include + +#include +#include +#include +#include + +#ifdef WIN32 +# include +#else +# include +#endif + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +static const std::string logPrefix_ = "[scwx::wsr88d::rpg::ar2v_file] "; + +class Ar2vFileImpl +{ +public: + explicit Ar2vFileImpl() : + tapeFilename_(), + extensionNumber_(), + julianDate_ {0}, + milliseconds_ {0}, + icao_(), + numRecords_ {0} {}; + ~Ar2vFileImpl() = default; + + void ParseLDMRecords(std::ifstream& f); + + std::string tapeFilename_; + std::string extensionNumber_; + int32_t julianDate_; + int32_t milliseconds_; + std::string icao_; + + size_t numRecords_; +}; + +Ar2vFile::Ar2vFile() : p(std::make_unique()) {} +Ar2vFile::~Ar2vFile() = default; + +Ar2vFile::Ar2vFile(Ar2vFile&&) noexcept = default; +Ar2vFile& Ar2vFile::operator=(Ar2vFile&&) = default; + +bool Ar2vFile::LoadFile(const std::string& filename) +{ + BOOST_LOG_TRIVIAL(debug) << logPrefix_ << "LoadFile(" << filename << ")\n"; + bool fileValid = true; + + std::ifstream f(filename, std::ios_base::in | std::ios_base::binary); + if (!f.good()) + { + BOOST_LOG_TRIVIAL(warning) + << logPrefix_ << "Could not open file for reading: " << filename; + fileValid = false; + } + + if (fileValid) + { + // Read Volume Header Record + p->tapeFilename_.resize(9, ' '); + p->extensionNumber_.resize(3, ' '); + p->icao_.resize(4, ' '); + + f.read(&p->tapeFilename_[0], 9); + f.read(&p->extensionNumber_[0], 3); + f.read(reinterpret_cast(&p->julianDate_), 4); + f.read(reinterpret_cast(&p->milliseconds_), 4); + f.read(&p->icao_[0], 4); + + p->julianDate_ = htonl(p->julianDate_); + p->milliseconds_ = htonl(p->milliseconds_); + } + + if (f.eof()) + { + BOOST_LOG_TRIVIAL(warning) + << logPrefix_ << "Could not read Volume Header Record\n"; + fileValid = false; + } + + if (fileValid) + { + BOOST_LOG_TRIVIAL(debug) + << logPrefix_ << "Filename: " << p->tapeFilename_; + BOOST_LOG_TRIVIAL(debug) + << logPrefix_ << "Extension: " << p->extensionNumber_; + BOOST_LOG_TRIVIAL(debug) << logPrefix_ << "Date: " << p->julianDate_; + BOOST_LOG_TRIVIAL(debug) + << logPrefix_ << "Time: " << p->milliseconds_; + BOOST_LOG_TRIVIAL(debug) << logPrefix_ << "ICAO: " << p->icao_; + + p->ParseLDMRecords(f); + } + + return fileValid; +} + +void Ar2vFileImpl::ParseLDMRecords(std::ifstream& f) +{ + numRecords_ = 0; + + while (f.peek() != EOF) + { + std::streampos startPosition = f.tellg(); + int32_t controlWord = 0; + size_t recordSize; + + f.read(reinterpret_cast(&controlWord), 4); + + controlWord = htonl(controlWord); + recordSize = std::abs(controlWord); + + BOOST_LOG_TRIVIAL(debug) + << logPrefix_ << "LDM Record Found: Size = " << recordSize << " bytes"; + + boost::iostreams::filtering_streambuf in; + util::rangebuf r(f.rdbuf(), recordSize); + in.push(boost::iostreams::bzip2_decompressor()); + in.push(r); + + std::ostringstream of; + + try + { + std::streamsize bytesCopied = boost::iostreams::copy(in, of); + BOOST_LOG_TRIVIAL(debug) + << logPrefix_ << "Decompressed record size = " << bytesCopied + << " bytes"; + } + catch (const boost::iostreams::bzip2_error& ex) + { + int error = ex.error(); + BOOST_LOG_TRIVIAL(warning) + << logPrefix_ << "Error decompressing record " << numRecords_; + + f.seekg(startPosition + std::streampos(recordSize)); + } + + ++numRecords_; + } + + BOOST_LOG_TRIVIAL(debug) + << logPrefix_ << "Found " << numRecords_ << " LDM Records"; +} + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake new file mode 100644 index 00000000..d4631139 --- /dev/null +++ b/wxdata/wxdata.cmake @@ -0,0 +1,31 @@ +project(scwx-data) + +find_package(Boost) + +set(HDR_UTIL include/scwx/util/rangebuf.hpp) +set(SRC_UTIL source/scwx/util/rangebuf.cpp) +set(HDR_WSR88D_RPG include/scwx/wsr88d/rpg/ar2v_file.hpp) +set(SRC_WSR88D_RPG source/scwx/wsr88d/rpg/ar2v_file.cpp) + +add_library(wxdata OBJECT ${HDR_UTIL} + ${SRC_UTIL} + ${HDR_WSR88D_RPG} + ${SRC_WSR88D_RPG}) + +source_group("Header Files\\util" FILES ${HDR_UTIL}) +source_group("Source Files\\util" FILES ${SRC_UTIL}) +source_group("Header Files\\wsr88d\\rpg" FILES ${HDR_WSR88D_RPG}) +source_group("Source Files\\wsr88d\\rpg" FILES ${SRC_WSR88D_RPG}) + +target_include_directories(wxdata PRIVATE ${Boost_INCLUDE_DIR} + ${scwx-data_SOURCE_DIR}/include + ${scwx-data_SOURCE_DIR}/source) +target_include_directories(wxdata INTERFACE ${scwx-data_SOURCE_DIR}/include) + +if(MSVC) + target_compile_options(wxdata PRIVATE /W3) +endif() + +set_target_properties(wxdata PROPERTIES CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF)