diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp new file mode 100644 index 00000000..c336dac7 --- /dev/null +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include +#include + +namespace scwx +{ +namespace awips +{ + +class TextProductMessageImpl; + +class TextProductMessage : public Message +{ +public: + explicit TextProductMessage(); + ~TextProductMessage(); + + TextProductMessage(const TextProductMessage&) = delete; + TextProductMessage& operator=(const TextProductMessage&) = delete; + + TextProductMessage(TextProductMessage&&) noexcept; + TextProductMessage& operator=(TextProductMessage&&) noexcept; + + size_t data_size() const; + + bool Parse(std::istream& is) override; + + static std::shared_ptr Create(std::istream& is); + +private: + std::unique_ptr p; +}; + +} // namespace awips +} // namespace scwx diff --git a/wxdata/include/scwx/common/characters.hpp b/wxdata/include/scwx/common/characters.hpp index fd73462d..896992cf 100644 --- a/wxdata/include/scwx/common/characters.hpp +++ b/wxdata/include/scwx/common/characters.hpp @@ -8,6 +8,7 @@ namespace Characters { constexpr char DEGREE = static_cast(0xb0); +constexpr char ETX = static_cast(0x03); } // namespace Characters } // namespace common diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp new file mode 100644 index 00000000..1b7bf749 --- /dev/null +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -0,0 +1,373 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace scwx +{ +namespace awips +{ + +static const std::string logPrefix_ = "[scwx::awips::text_product_message] "; + +// Issuance date/time takes one of the following forms: +// * _xM__day_mon_
_year +// * _UTC_day_mon_
_year +// Segment Header only: +// * _xM__day_mon_
_year_/_xM__day_mon_
_year/ +// Look for hhmm (xM|UTC) to key the date/time string +static const std::regex reDateTimeString {"^[0-9]{3,4} ([AP]M|UTC)"}; + +struct Vtec +{ + std::string pVtec_; + std::string hVtec_; + + Vtec() : pVtec_ {}, hVtec_ {} {} + + Vtec(const Vtec&) = delete; + Vtec& operator=(const Vtec&) = delete; + + Vtec(Vtec&&) noexcept = default; + Vtec& operator=(Vtec&&) noexcept = default; +}; + +struct SegmentHeader +{ + std::string ugcString_; + std::vector vtecString_; + std::vector ugcNames_; + std::string issuanceDateTime_; + + SegmentHeader() : + ugcString_ {}, vtecString_ {}, ugcNames_ {}, issuanceDateTime_ {} + { + } + + SegmentHeader(const SegmentHeader&) = delete; + SegmentHeader& operator=(const SegmentHeader&) = delete; + + SegmentHeader(SegmentHeader&&) noexcept = default; + SegmentHeader& operator=(SegmentHeader&&) noexcept = default; +}; + +struct Segment +{ + std::optional header_; + std::vector productContent_; + + Segment() : header_ {}, productContent_ {} {} + + Segment(const Segment&) = delete; + Segment& operator=(const Segment&) = delete; + + Segment(Segment&&) noexcept = default; + Segment& operator=(Segment&&) noexcept = default; +}; + +static std::vector ParseProductContent(std::istream& is); +void SkipBlankLines(std::istream& is); +bool TryParseEndOfProduct(std::istream& is); +static std::vector TryParseMndHeader(std::istream& is); +static std::optional TryParseSegmentHeader(std::istream& is); +static std::optional TryParseVtecString(std::istream& is); + +class TextProductMessageImpl +{ +public: + explicit TextProductMessageImpl() : wmoHeader_ {} {} + ~TextProductMessageImpl() = default; + + std::shared_ptr wmoHeader_; + std::vector mndHeader_; + std::vector> segments_; +}; + +TextProductMessage::TextProductMessage() : + p(std::make_unique()) +{ +} +TextProductMessage::~TextProductMessage() = default; + +TextProductMessage::TextProductMessage(TextProductMessage&&) noexcept = default; +TextProductMessage& +TextProductMessage::operator=(TextProductMessage&&) noexcept = default; + +size_t TextProductMessage::data_size() const +{ + return 0; +} + +bool TextProductMessage::Parse(std::istream& is) +{ + bool dataValid = true; + + p->wmoHeader_ = std::make_shared(); + dataValid = p->wmoHeader_->Parse(is); + + for (size_t i = 0; dataValid && !is.eof(); i++) + { + if (i != 0 && TryParseEndOfProduct(is)) + { + break; + } + + std::shared_ptr segment = std::make_shared(); + + if (i == 0) + { + if (is.peek() != '\r') + { + segment->header_ = TryParseSegmentHeader(is); + } + + SkipBlankLines(is); + + p->mndHeader_ = TryParseMndHeader(is); + SkipBlankLines(is); + } + + if (!segment->header_.has_value()) + { + segment->header_ = TryParseSegmentHeader(is); + SkipBlankLines(is); + } + + segment->productContent_ = ParseProductContent(is); + SkipBlankLines(is); + + if (segment->header_.has_value() || !segment->productContent_.empty()) + { + p->segments_.push_back(std::move(segment)); + } + } + + return dataValid; +} + +std::vector ParseProductContent(std::istream& is) +{ + std::vector productContent; + std::string line; + + while (!is.eof() && is.peek() != common::Characters::ETX) + { + util::getline(is, line); + + productContent.push_back(line); + + if (line.starts_with("$$")) + { + // End of Product or Product Segment Code + break; + } + } + + while (!productContent.empty() && productContent.back().empty()) + { + productContent.pop_back(); + } + + return productContent; +} + +void SkipBlankLines(std::istream& is) +{ + std::string line; + + while (is.peek() == '\r') + { + util::getline(is, line); + } +} + +bool TryParseEndOfProduct(std::istream& is) +{ + std::string line; + std::streampos isBegin = is.tellg(); + bool endOfStream = false; + + if (is.peek() == common::Characters::ETX) + { + is.get(); + endOfStream = true; + } + else if (is.peek() == EOF) + { + endOfStream = true; + } + + if (!endOfStream) + { + // Optional Forecast Identifier + util::getline(is, line); + SkipBlankLines(is); + + if (is.peek() == common::Characters::ETX) + { + is.get(); + endOfStream = true; + } + else if (is.peek() == EOF) + { + endOfStream = true; + } + } + + if (!endOfStream) + { + // End of Product was not found, so reset the istream to the original + // state + is.seekg(isBegin, std::ios_base::beg); + } + + return endOfStream; +} + +std::vector TryParseMndHeader(std::istream& is) +{ + std::vector mndHeader; + std::string line; + std::streampos isBegin = is.tellg(); + + while (!is.eof() && is.peek() != '\r') + { + util::getline(is, line); + mndHeader.push_back(line); + } + + if (!mndHeader.empty() && + !std::regex_search(mndHeader.back(), reDateTimeString)) + { + // MND Header should end with an Issuance Date/Time Line + mndHeader.clear(); + } + + if (mndHeader.empty()) + { + // MND header was not found, so reset the istream to the original state + is.seekg(isBegin, std::ios_base::beg); + } + + return mndHeader; +} + +std::optional TryParseSegmentHeader(std::istream& is) +{ + // UGC takes the form SSFNNN-NNN>NNN-SSFNNN-DDHHMM- (NWSI 10-1702) + // Look for SSF(NNN)?[->] to key the UGC string + static const std::regex reUgcString {"^[A-Z]{2}[CZ]([0-9]{3})?[->]"}; + + std::optional header = std::nullopt; + std::string line; + std::streampos isBegin = is.tellg(); + + util::getline(is, line); + + if (std::regex_search(line, reUgcString)) + { + header = SegmentHeader(); + header->ugcString_.swap(line); + } + + if (header.has_value()) + { + std::optional vtec; + while ((vtec = TryParseVtecString(is)) != std::nullopt) + { + header->vtecString_.push_back(std::move(*vtec)); + } + + while (!is.eof() && is.peek() != '\r') + { + util::getline(is, line); + if (!std::regex_search(line, reDateTimeString)) + { + header->ugcNames_.push_back(line); + } + else + { + header->issuanceDateTime_.swap(line); + break; + } + } + } + + if (!header.has_value()) + { + // We did not find a valid segment header, so we reset the istream to the + // original state + is.seekg(isBegin, std::ios_base::beg); + } + + return header; +} + +std::optional TryParseVtecString(std::istream& is) +{ + // P-VTEC takes the form /k.aaa.cccc.pp.s.####.yymmddThhnnZB-yymmddThhnnZE/ + // (NWSI 10-1703) + // Look for /k. to key the P-VTEC string + static const std::regex rePVtecString {"^/[OTEX]\\."}; + + // H-VTEC takes the form + // /nwsli.s.ic.yymmddThhnnZB.yymmddThhnnZC.yymmddThhnnZE.fr/ (NWSI 10-1703) + // Look for /nwsli. to key the H-VTEC string + static const std::regex reHVtecString {"^/[A-Z0-9]{5}\\."}; + + std::optional vtec = std::nullopt; + std::string line; + std::streampos isBegin = is.tellg(); + + util::getline(is, line); + + if (std::regex_search(line, rePVtecString)) + { + vtec = Vtec(); + vtec->pVtec_.swap(line); + + isBegin = is.tellg(); + + util::getline(is, line); + + if (std::regex_search(line, reHVtecString)) + { + vtec->hVtec_.swap(line); + } + else + { + // H-VTEC was not found, so reset the istream to the beginning of the + // line + is.seekg(isBegin, std::ios_base::beg); + } + } + else + { + // P-VTEC was not found, so reset the istream to the original state + is.seekg(isBegin, std::ios_base::beg); + } + + return vtec; +} + +std::shared_ptr TextProductMessage::Create(std::istream& is) +{ + std::shared_ptr message = + std::make_shared(); + + if (!message->Parse(is)) + { + message.reset(); + } + + return message; +} + +} // namespace awips +} // namespace scwx diff --git a/wxdata/source/scwx/util/streams.cpp b/wxdata/source/scwx/util/streams.cpp index 51bb9de0..9e094f9b 100644 --- a/wxdata/source/scwx/util/streams.cpp +++ b/wxdata/source/scwx/util/streams.cpp @@ -20,6 +20,10 @@ std::istream& getline(std::istream& is, std::string& t) case '\n': return is; case '\r': + while (sb->sgetc() == '\r') + { + sb->sbumpc(); + } if (sb->sgetc() == '\n') { sb->sbumpc(); diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 849a6a49..65ec6117 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -3,8 +3,10 @@ project(scwx-data) find_package(Boost) set(HDR_AWIPS include/scwx/awips/message.hpp + include/scwx/awips/text_product_message.hpp include/scwx/awips/wmo_header.hpp) set(SRC_AWIPS source/scwx/awips/message.cpp + source/scwx/awips/text_product_message.cpp source/scwx/awips/wmo_header.cpp) set(HDR_COMMON include/scwx/common/characters.hpp include/scwx/common/color_table.hpp