From db4f37a37de232575547365fc47a8256277cfaac Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 26 Jan 2022 13:52:14 -0600 Subject: [PATCH] Coded time motion location --- .../awips/coded_time_motion_location.test.cpp | 90 +++++++++ test/test.cmake | 1 + .../scwx/awips/coded_time_motion_location.hpp | 46 +++++ .../scwx/awips/coded_time_motion_location.cpp | 183 ++++++++++++++++++ wxdata/wxdata.cmake | 2 + 5 files changed, 322 insertions(+) create mode 100644 test/source/scwx/awips/coded_time_motion_location.test.cpp create mode 100644 wxdata/include/scwx/awips/coded_time_motion_location.hpp create mode 100644 wxdata/source/scwx/awips/coded_time_motion_location.cpp diff --git a/test/source/scwx/awips/coded_time_motion_location.test.cpp b/test/source/scwx/awips/coded_time_motion_location.test.cpp new file mode 100644 index 00000000..931de2d2 --- /dev/null +++ b/test/source/scwx/awips/coded_time_motion_location.test.cpp @@ -0,0 +1,90 @@ +#include + +#include + +#include + +namespace scwx +{ +namespace awips +{ + +static const std::string logPrefix_ = + "[scwx::awips::coded_time_motion_location.test] "; + +TEST(CodedTimeMotionLocation, LeadingZeroes) +{ + using namespace std::chrono; + + std::vector data = { + "TIME...MOT...LOC 0128Z 004DEG 9KT 3480 10318"}; + + CodedTimeMotionLocation tml; + bool dataValid = tml.Parse(data); + + ASSERT_EQ(dataValid, true); + + EXPECT_EQ(tml.time().to_duration(), 1h + 28min); + EXPECT_EQ(tml.direction(), 4); + EXPECT_EQ(tml.speed(), 9); + + auto coordinates = tml.coordinates(); + + ASSERT_EQ(coordinates.size(), 1); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 34.80); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -103.18); +} + +TEST(CodedTimeMotionLocation, Stationary) +{ + using namespace std::chrono; + + std::vector data = { + "TIME...MOT...LOC 1959Z 254DEG 0KT 3253 11464"}; + + CodedTimeMotionLocation tml; + bool dataValid = tml.Parse(data); + + ASSERT_EQ(dataValid, true); + + EXPECT_EQ(tml.time().to_duration(), 19h + 59min); + EXPECT_EQ(tml.direction(), 254); + EXPECT_EQ(tml.speed(), 0); + + auto coordinates = tml.coordinates(); + + ASSERT_EQ(coordinates.size(), 1); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 32.53); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -114.64); +} + +TEST(CodedTimeMotionLocation, TwoCoordinates) +{ + using namespace std::chrono; + + std::vector data = { + "TIME...MOT...LOC 2113Z 345DEG 42KT 2760 8211 2724 8198"}; + + CodedTimeMotionLocation tml; + bool dataValid = tml.Parse(data); + + ASSERT_EQ(dataValid, true); + + EXPECT_EQ(tml.time().to_duration(), 21h + 13min); + EXPECT_EQ(tml.direction(), 345); + EXPECT_EQ(tml.speed(), 42); + + auto coordinates = tml.coordinates(); + + ASSERT_EQ(coordinates.size(), 2); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 27.6); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -82.11); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 27.24); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, -81.98); +} + +} // namespace awips +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index 6950b9e3..7f7e662f 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -9,6 +9,7 @@ find_package(GTest) set(SRC_MAIN source/scwx/wxtest.cpp) set(SRC_AWIPS_TESTS source/scwx/awips/coded_location.test.cpp + source/scwx/awips/coded_time_motion_location.test.cpp source/scwx/awips/pvtec.test.cpp source/scwx/awips/text_product_file.test.cpp) set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp) diff --git a/wxdata/include/scwx/awips/coded_time_motion_location.hpp b/wxdata/include/scwx/awips/coded_time_motion_location.hpp new file mode 100644 index 00000000..41e4bfb5 --- /dev/null +++ b/wxdata/include/scwx/awips/coded_time_motion_location.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace awips +{ + +class CodedTimeMotionLocationImpl; + +class CodedTimeMotionLocation +{ +public: + typedef boost::any_range + StringRange; + + explicit CodedTimeMotionLocation(); + ~CodedTimeMotionLocation(); + + CodedTimeMotionLocation(const CodedTimeMotionLocation&) = delete; + CodedTimeMotionLocation& operator=(const CodedTimeMotionLocation&) = delete; + + CodedTimeMotionLocation(CodedTimeMotionLocation&&) noexcept; + CodedTimeMotionLocation& operator=(CodedTimeMotionLocation&&) noexcept; + + std::chrono::hh_mm_ss time() const; + uint16_t direction() const; + uint8_t speed() const; + std::vector coordinates() const; + + bool Parse(const StringRange& lines, const std::string& wfo = ""); + +private: + std::unique_ptr p; +}; + +} // namespace awips +} // namespace scwx diff --git a/wxdata/source/scwx/awips/coded_time_motion_location.cpp b/wxdata/source/scwx/awips/coded_time_motion_location.cpp new file mode 100644 index 00000000..054404f3 --- /dev/null +++ b/wxdata/source/scwx/awips/coded_time_motion_location.cpp @@ -0,0 +1,183 @@ +// Enable chrono formatters +#ifndef __cpp_lib_format +# define __cpp_lib_format 202110L +#endif + +#include + +#include + +#include + +namespace scwx +{ +namespace awips +{ + +static const std::string logPrefix_ = + "[scwx::awips::coded_time_motion_location] "; + +class CodedTimeMotionLocationImpl +{ +public: + explicit CodedTimeMotionLocationImpl() : + time_ {}, direction_ {0}, speed_ {0}, coordinates_ {} + { + } + + ~CodedTimeMotionLocationImpl() {} + + std::chrono::hh_mm_ss time_; + uint16_t direction_; + uint8_t speed_; + std::vector coordinates_; +}; + +CodedTimeMotionLocation::CodedTimeMotionLocation() : + p(std::make_unique()) +{ +} +CodedTimeMotionLocation::~CodedTimeMotionLocation() = default; + +CodedTimeMotionLocation::CodedTimeMotionLocation( + CodedTimeMotionLocation&&) noexcept = default; +CodedTimeMotionLocation& CodedTimeMotionLocation::operator=( + CodedTimeMotionLocation&&) noexcept = default; + +std::chrono::hh_mm_ss +CodedTimeMotionLocation::time() const +{ + return p->time_; +} + +uint16_t CodedTimeMotionLocation::direction() const +{ + return p->direction_; +} + +uint8_t CodedTimeMotionLocation::speed() const +{ + return p->speed_; +} + +std::vector CodedTimeMotionLocation::coordinates() const +{ + return p->coordinates_; +} + +bool CodedTimeMotionLocation::Parse(const StringRange& lines, + const std::string& wfo) +{ + bool dataValid = true; + + std::vector tokenList; + + for (std::string line : lines) + { + std::string token; + std::istringstream tokenStream {line}; + + while (tokenStream >> token) + { + tokenList.push_back(token); + } + } + + // First token is "TIME...MOT...LOC" + // At a minimum, one point (latitude/longitude pair) will be included + dataValid = (tokenList.size() >= 6 && tokenList.size() % 2 == 0 && + tokenList.at(0) == "TIME...MOT...LOC"); + + if (dataValid) + { + const bool wfoIsWest = (wfo != "PGUM"); + double westLongitude = (wfoIsWest) ? -1.0 : 1.0; + bool straddlesDateLine = false; + + // Time: hhmmZ + std::string time = tokenList.at(1); + { + using namespace std::chrono; + + static const std::string timeFormat {"%H%MZ"}; + + std::istringstream in {time}; + minutes tp; + in >> parse(timeFormat, tp); + + if (time.size() == 5 && !in.fail()) + { + p->time_ = hh_mm_ss {tp}; + } + else + { + BOOST_LOG_TRIVIAL(warning) + << logPrefix_ << "Invalid time: \"" << time << "\""; + p->time_ = hh_mm_ss {}; + dataValid = false; + } + } + + // Direction: dirDEG + std::string direction = tokenList.at(2); + if (direction.size() == 6 && direction.ends_with("DEG")) + { + p->direction_ = + static_cast(std::stoul(direction.substr(0, 3))); + } + else + { + BOOST_LOG_TRIVIAL(warning) + << logPrefix_ << "Invalid direction: \"" << direction << "\""; + dataValid = false; + } + + // Speed: KT + std::string speed = tokenList.at(3); + if (speed.size() >= 3 && speed.size() <= 4 && speed.ends_with("KT")) + { + p->speed_ = + static_cast(std::stoul(speed.substr(0, speed.size() - 2))); + } + else + { + BOOST_LOG_TRIVIAL(warning) + << logPrefix_ << "Invalid speed: \"" << speed << "\""; + dataValid = false; + } + + // Location + for (auto token = tokenList.cbegin() + 4; token != tokenList.cend(); + ++token) + { + double latitude = std::stod(*token) * 0.01; + ++token; + double longitude = std::stod(*token) * 0.01; + + // If a given product straddles 180 degrees longitude, those points + // west of 180 degrees will be given as if they were west longitude + if (longitude > 180.0) + { + longitude -= 360.0; + straddlesDateLine = true; + } + + longitude *= westLongitude; + + p->coordinates_.push_back({latitude, longitude}); + } + + if (!wfoIsWest && straddlesDateLine) + { + for (auto& coordinate : p->coordinates_) + { + coordinate.longitude_ *= -1.0; + } + } + } + + return dataValid; +} + +} // namespace awips +} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 731b0dd4..20165e0f 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -3,6 +3,7 @@ project(scwx-data) find_package(Boost) set(HDR_AWIPS include/scwx/awips/coded_location.hpp + include/scwx/awips/coded_time_motion_location.hpp include/scwx/awips/message.hpp include/scwx/awips/phenomenon.hpp include/scwx/awips/pvtec.hpp @@ -11,6 +12,7 @@ set(HDR_AWIPS include/scwx/awips/coded_location.hpp include/scwx/awips/text_product_message.hpp include/scwx/awips/wmo_header.hpp) set(SRC_AWIPS source/scwx/awips/coded_location.cpp + source/scwx/awips/coded_time_motion_location.cpp source/scwx/awips/message.cpp source/scwx/awips/phenomenon.cpp source/scwx/awips/pvtec.cpp