From ecf5b1f5f0d839e922320112d5c77008259e4147 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 25 Jan 2022 21:39:28 -0600 Subject: [PATCH] Coded location --- .../source/scwx/awips/coded_location.test.cpp | 199 ++++++++++++++++++ test/test.cmake | 3 +- wxdata/include/scwx/awips/coded_location.hpp | 42 ++++ wxdata/include/scwx/common/geographic.hpp | 25 +++ wxdata/source/scwx/awips/coded_location.cpp | 155 ++++++++++++++ wxdata/wxdata.cmake | 7 +- 6 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 test/source/scwx/awips/coded_location.test.cpp create mode 100644 wxdata/include/scwx/awips/coded_location.hpp create mode 100644 wxdata/include/scwx/common/geographic.hpp create mode 100644 wxdata/source/scwx/awips/coded_location.cpp diff --git a/test/source/scwx/awips/coded_location.test.cpp b/test/source/scwx/awips/coded_location.test.cpp new file mode 100644 index 00000000..a3ef5e80 --- /dev/null +++ b/test/source/scwx/awips/coded_location.test.cpp @@ -0,0 +1,199 @@ +#include + +#include + +#include + +namespace scwx +{ +namespace awips +{ + +static const std::string logPrefix_ = "[scwx::awips::coded_location.test] "; + +TEST(CodedLocation, WFO100W) +{ + std::vector data = { + "LAT...LON 4896 10015 4789 10017 4787 9995 4842 9987", + " 4842 9955 4897 9958"}; + + CodedLocation location; + bool dataValid = location.Parse(data); + + ASSERT_EQ(dataValid, true); + + auto coordinates = location.coordinates(); + + ASSERT_EQ(coordinates.size(), 6); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 48.96); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -100.15); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 47.89); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, -100.17); + EXPECT_DOUBLE_EQ(coordinates[2].latitude_, 47.87); + EXPECT_DOUBLE_EQ(coordinates[2].longitude_, -99.95); + EXPECT_DOUBLE_EQ(coordinates[3].latitude_, 48.42); + EXPECT_DOUBLE_EQ(coordinates[3].longitude_, -99.87); + EXPECT_DOUBLE_EQ(coordinates[4].latitude_, 48.42); + EXPECT_DOUBLE_EQ(coordinates[4].longitude_, -99.55); + EXPECT_DOUBLE_EQ(coordinates[5].latitude_, 48.97); + EXPECT_DOUBLE_EQ(coordinates[5].longitude_, -99.58); +} + +TEST(CodedLocation, WFOWestWrap) +{ + std::vector data = { + "LAT...LON 4896 10015 4789 18100 4787 9995 4842 9987", + " 4842 9955 4897 9958"}; + + CodedLocation location; + bool dataValid = location.Parse(data); + + ASSERT_EQ(dataValid, true); + + auto coordinates = location.coordinates(); + + ASSERT_EQ(coordinates.size(), 6); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 48.96); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -100.15); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 47.89); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, 179.00); + EXPECT_DOUBLE_EQ(coordinates[2].latitude_, 47.87); + EXPECT_DOUBLE_EQ(coordinates[2].longitude_, -99.95); + EXPECT_DOUBLE_EQ(coordinates[3].latitude_, 48.42); + EXPECT_DOUBLE_EQ(coordinates[3].longitude_, -99.87); + EXPECT_DOUBLE_EQ(coordinates[4].latitude_, 48.42); + EXPECT_DOUBLE_EQ(coordinates[4].longitude_, -99.55); + EXPECT_DOUBLE_EQ(coordinates[5].latitude_, 48.97); + EXPECT_DOUBLE_EQ(coordinates[5].longitude_, -99.58); +} + +TEST(CodedLocation, WFOGuam) +{ + std::vector data = { + "LAT...LON 1360 14509 1371 14495 1348 14463 1325 14492"}; + + CodedLocation location; + bool dataValid = location.Parse(data, "PGUM"); + + ASSERT_EQ(dataValid, true); + + auto coordinates = location.coordinates(); + + ASSERT_EQ(coordinates.size(), 4); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 13.60); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, 145.09); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 13.71); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, 144.95); + EXPECT_DOUBLE_EQ(coordinates[2].latitude_, 13.48); + EXPECT_DOUBLE_EQ(coordinates[2].longitude_, 144.63); + EXPECT_DOUBLE_EQ(coordinates[3].latitude_, 13.25); + EXPECT_DOUBLE_EQ(coordinates[3].longitude_, 144.92); +} + +TEST(CodedLocation, WFOGuamWrap) +{ + std::vector data = { + "LAT...LON 1360 14509 1371 18195 1348 14463 1325 14492"}; + + CodedLocation location; + bool dataValid = location.Parse(data, "PGUM"); + + ASSERT_EQ(dataValid, true); + + auto coordinates = location.coordinates(); + + ASSERT_EQ(coordinates.size(), 4); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 13.60); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -145.09); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 13.71); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, 178.05); + EXPECT_DOUBLE_EQ(coordinates[2].latitude_, 13.48); + EXPECT_DOUBLE_EQ(coordinates[2].longitude_, -144.63); + EXPECT_DOUBLE_EQ(coordinates[3].latitude_, 13.25); + EXPECT_DOUBLE_EQ(coordinates[3].longitude_, -144.92); +} + +TEST(CodedLocation, NC100W) +{ + std::vector data = { + "LAT...LON 46680254 49089563 47069563 44650254"}; + + CodedLocation location; + bool dataValid = location.Parse(data); + + ASSERT_EQ(dataValid, true); + + auto coordinates = location.coordinates(); + + ASSERT_EQ(coordinates.size(), 4); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 46.68); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -102.54); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 49.08); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, -95.63); + EXPECT_DOUBLE_EQ(coordinates[2].latitude_, 47.06); + EXPECT_DOUBLE_EQ(coordinates[2].longitude_, -95.63); + EXPECT_DOUBLE_EQ(coordinates[3].latitude_, 44.65); + EXPECT_DOUBLE_EQ(coordinates[3].longitude_, -102.54); +} + +TEST(CodedLocation, NCWashington) +{ + std::vector data = { + "LAT...LON 49112272 49092189 48662129 48022117 47452115", + " 47072129 46622149 46502228 46292349 46442456", + " 46722439 47802499 48522503 48422448 48252386", + " 48332313"}; + + CodedLocation location; + + bool dataValid = location.Parse(data); + + ASSERT_EQ(dataValid, true); + + auto coordinates = location.coordinates(); + + ASSERT_EQ(coordinates.size(), 16); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 49.11); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -122.72); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 49.09); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, -121.89); + EXPECT_DOUBLE_EQ(coordinates[14].latitude_, 48.25); + EXPECT_DOUBLE_EQ(coordinates[14].longitude_, -123.86); + EXPECT_DOUBLE_EQ(coordinates[15].latitude_, 48.33); + EXPECT_DOUBLE_EQ(coordinates[15].longitude_, -123.13); +} + +TEST(CodedLocation, NCMaine) +{ + std::vector data = { + "LAT...LON 47316870 47216795 46466767 45436766 44756779", + " 44216834 43816943 43706970 43837006 44497009", + " 45306974 46356946 46976921"}; + + CodedLocation location; + bool dataValid = location.Parse(data); + + ASSERT_EQ(dataValid, true); + + auto coordinates = location.coordinates(); + + ASSERT_EQ(coordinates.size(), 13); + + EXPECT_DOUBLE_EQ(coordinates[0].latitude_, 47.31); + EXPECT_DOUBLE_EQ(coordinates[0].longitude_, -68.70); + EXPECT_DOUBLE_EQ(coordinates[1].latitude_, 47.21); + EXPECT_DOUBLE_EQ(coordinates[1].longitude_, -67.95); + EXPECT_DOUBLE_EQ(coordinates[11].latitude_, 46.35); + EXPECT_DOUBLE_EQ(coordinates[11].longitude_, -69.46); + EXPECT_DOUBLE_EQ(coordinates[12].latitude_, 46.97); + EXPECT_DOUBLE_EQ(coordinates[12].longitude_, -69.21); +} + +} // namespace awips +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index e776fcef..6950b9e3 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -8,7 +8,8 @@ find_package(BZip2) find_package(GTest) set(SRC_MAIN source/scwx/wxtest.cpp) -set(SRC_AWIPS_TESTS source/scwx/awips/pvtec.test.cpp +set(SRC_AWIPS_TESTS source/scwx/awips/coded_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) set(SRC_QT_MANAGER_TESTS source/scwx/qt/manager/settings_manager.test.cpp) diff --git a/wxdata/include/scwx/awips/coded_location.hpp b/wxdata/include/scwx/awips/coded_location.hpp new file mode 100644 index 00000000..45cfade2 --- /dev/null +++ b/wxdata/include/scwx/awips/coded_location.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +namespace scwx +{ +namespace awips +{ + +class CodedLocationImpl; + +class CodedLocation +{ +public: + typedef boost::any_range + StringRange; + + explicit CodedLocation(); + ~CodedLocation(); + + CodedLocation(const CodedLocation&) = delete; + CodedLocation& operator=(const CodedLocation&) = delete; + + CodedLocation(CodedLocation&&) noexcept; + CodedLocation& operator=(CodedLocation&&) noexcept; + + 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/include/scwx/common/geographic.hpp b/wxdata/include/scwx/common/geographic.hpp new file mode 100644 index 00000000..29c6c367 --- /dev/null +++ b/wxdata/include/scwx/common/geographic.hpp @@ -0,0 +1,25 @@ +#pragma once + +namespace scwx +{ +namespace common +{ + +struct Coordinate +{ + double latitude_; + double longitude_; + + Coordinate(double latitude, double longitude) : + latitude_ {latitude}, longitude_ {longitude} + { + } + + bool operator==(const Coordinate& o) const + { + return latitude_ == o.latitude_ && longitude_ == o.longitude_; + } +}; + +} // namespace common +} // namespace scwx diff --git a/wxdata/source/scwx/awips/coded_location.cpp b/wxdata/source/scwx/awips/coded_location.cpp new file mode 100644 index 00000000..f70b1fb7 --- /dev/null +++ b/wxdata/source/scwx/awips/coded_location.cpp @@ -0,0 +1,155 @@ +#include + +#include + +#include + +namespace scwx +{ +namespace awips +{ + +static const std::string logPrefix_ = "[scwx::awips::coded_location] "; + +class CodedLocationImpl +{ +public: + explicit CodedLocationImpl() : coordinates_ {} {} + + ~CodedLocationImpl() {} + + std::vector coordinates_; +}; + +CodedLocation::CodedLocation() : p(std::make_unique()) {} +CodedLocation::~CodedLocation() = default; + +CodedLocation::CodedLocation(CodedLocation&&) noexcept = default; +CodedLocation& CodedLocation::operator=(CodedLocation&&) noexcept = default; + +std::vector CodedLocation::coordinates() const +{ + return p->coordinates_; +} + +bool CodedLocation::Parse(const StringRange& lines, const std::string& wfo) +{ + enum class LocationFormat + { + WFO, + NationalCenter + }; + + bool dataValid = true; + LocationFormat format; + + 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 "LAT...LON" + // At a minimum, three points (latitude/longitude pairs) will be included + dataValid = (tokenList.size() >= 4 && tokenList.at(0) == "LAT...LON"); + + if (dataValid) + { + format = (tokenList.at(1).size() == 8) ? LocationFormat::NationalCenter : + LocationFormat::WFO; + + if (format == LocationFormat::WFO) + { + dataValid = (tokenList.size() >= 7 && tokenList.size() % 2 == 1); + } + } + + if (dataValid) + { + if (format == LocationFormat::WFO) + { + const bool wfoIsWest = (wfo != "PGUM"); + double westLongitude = (wfoIsWest) ? -1.0 : 1.0; + bool straddlesDateLine = false; + + for (auto token = tokenList.cbegin() + 1; 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; + } + } + } + else + { + for (auto token = tokenList.cbegin() + 1; token != tokenList.cend(); + ++token) + { + if (token->size() != 8) + { + dataValid = false; + break; + } + + double latitude = std::stod(token->substr(0, 4)) * 0.01; + double longitude = std::stod(token->substr(4, 4)) * -0.01; + + // Longitudes of greater than 100 degrees will drop the leading 1; + // i.e., 105.22 W would be coded as 0522. This is ambiguous + // with 5.22 W, so we assume everything east of 65 W (east of Maine, + // easternmost point of CONUS) should have 100 degrees added to it. + // Points in the Atlantic or western Alaska will not be correct, but + // it is assumed that products will not contain those points coded + // using this methodology. + if (longitude > -65.0) + { + longitude -= 100.0; + } + + p->coordinates_.push_back({latitude, longitude}); + } + } + } + + if (dataValid) + { + // If the last point is a repeat of the first point, remove it as + // redundant + if (p->coordinates_.front() == p->coordinates_.back()) + { + p->coordinates_.pop_back(); + } + } + + return dataValid; +} + +} // namespace awips +} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index a529a836..731b0dd4 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -2,14 +2,16 @@ project(scwx-data) find_package(Boost) -set(HDR_AWIPS include/scwx/awips/message.hpp +set(HDR_AWIPS include/scwx/awips/coded_location.hpp + include/scwx/awips/message.hpp include/scwx/awips/phenomenon.hpp include/scwx/awips/pvtec.hpp include/scwx/awips/significance.hpp include/scwx/awips/text_product_file.hpp include/scwx/awips/text_product_message.hpp include/scwx/awips/wmo_header.hpp) -set(SRC_AWIPS source/scwx/awips/message.cpp +set(SRC_AWIPS source/scwx/awips/coded_location.cpp + source/scwx/awips/message.cpp source/scwx/awips/phenomenon.cpp source/scwx/awips/pvtec.cpp source/scwx/awips/significance.cpp @@ -19,6 +21,7 @@ set(SRC_AWIPS source/scwx/awips/message.cpp set(HDR_COMMON include/scwx/common/characters.hpp include/scwx/common/color_table.hpp include/scwx/common/constants.hpp + include/scwx/common/geographic.hpp include/scwx/common/products.hpp include/scwx/common/types.hpp include/scwx/common/vcp.hpp)