diff --git a/wxdata/include/scwx/gr/color.hpp b/wxdata/include/scwx/gr/color.hpp new file mode 100644 index 00000000..787a7649 --- /dev/null +++ b/wxdata/include/scwx/gr/color.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace scwx +{ +namespace gr +{ + +boost::gil::rgba8_pixel_t ParseColor(const std::vector& tokenList, + std::size_t startIndex, + ColorMode colorMode, + bool hasAlpha = true); + +} // namespace gr +} // namespace scwx diff --git a/wxdata/include/scwx/gr/gr_types.hpp b/wxdata/include/scwx/gr/gr_types.hpp new file mode 100644 index 00000000..e90c1042 --- /dev/null +++ b/wxdata/include/scwx/gr/gr_types.hpp @@ -0,0 +1,15 @@ +#pragma once + +namespace scwx +{ +namespace gr +{ + +enum class ColorMode +{ + RGBA, + HSLuv +}; + +} // namespace gr +} // namespace scwx diff --git a/wxdata/include/scwx/gr/placefile.hpp b/wxdata/include/scwx/gr/placefile.hpp new file mode 100644 index 00000000..33b7fff5 --- /dev/null +++ b/wxdata/include/scwx/gr/placefile.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace gr +{ + +/** + * @brief Place File + * + * Implementation based on: + * Place File Specification + * Mike Gibson + * Gibson Ridge Software, LLC. Used with permission. + * http://www.grlevelx.com/manuals/gis/files_places.htm + */ +class Placefile +{ +public: + explicit Placefile(); + ~Placefile(); + + Placefile(const Placefile&) = delete; + Placefile& operator=(const Placefile&) = delete; + + Placefile(Placefile&&) noexcept; + Placefile& operator=(Placefile&&) noexcept; + + bool IsValid() const; + + static std::shared_ptr Load(const std::string& filename); + static std::shared_ptr Load(std::istream& is); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace common +} // namespace scwx diff --git a/wxdata/source/scwx/gr/color.cpp b/wxdata/source/scwx/gr/color.cpp new file mode 100644 index 00000000..5f815e6d --- /dev/null +++ b/wxdata/source/scwx/gr/color.cpp @@ -0,0 +1,86 @@ +#include + +#include + +#include + +namespace scwx +{ +namespace gr +{ + +template +T RoundChannel(double value); +template +T StringToDecimal(const std::string& str); + +boost::gil::rgba8_pixel_t ParseColor(const std::vector& tokenList, + std::size_t startIndex, + ColorMode colorMode, + bool hasAlpha) +{ + + std::uint8_t r {}; + std::uint8_t g {}; + std::uint8_t b {}; + std::uint8_t a = 255; + + if (colorMode == ColorMode::RGBA) + { + if (tokenList.size() >= startIndex + 3) + { + r = StringToDecimal(tokenList[startIndex + 0]); + g = StringToDecimal(tokenList[startIndex + 1]); + b = StringToDecimal(tokenList[startIndex + 2]); + } + + if (hasAlpha && tokenList.size() >= startIndex + 4) + { + a = StringToDecimal(tokenList[startIndex + 3]); + } + } + else // if (colorMode == ColorMode::HSLuv) + { + double h {}; + double s {}; + double l {}; + + if (tokenList.size() >= startIndex + 3) + { + h = std::stod(tokenList[startIndex + 0]); + s = std::stod(tokenList[startIndex + 1]); + l = std::stod(tokenList[startIndex + 2]); + } + + double dr; + double dg; + double db; + + hsluv2rgb(h, s, l, &dr, &dg, &db); + + r = RoundChannel(dr * 255.0); + g = RoundChannel(dg * 255.0); + b = RoundChannel(db * 255.0); + } + + return boost::gil::rgba8_pixel_t {r, g, b, a}; +} + +template +T RoundChannel(double value) +{ + return static_cast(std::clamp(std::lround(value), + std::numeric_limits::min(), + std::numeric_limits::max())); +} + +template +T StringToDecimal(const std::string& str) +{ + return static_cast(std::clamp(std::stoi(str), + std::numeric_limits::min(), + std::numeric_limits::max())); +} + +} // namespace gr +} // namespace scwx diff --git a/wxdata/source/scwx/gr/placefile.cpp b/wxdata/source/scwx/gr/placefile.cpp new file mode 100644 index 00000000..3fbe1f08 --- /dev/null +++ b/wxdata/source/scwx/gr/placefile.cpp @@ -0,0 +1,391 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace gr +{ + +static const std::string logPrefix_ {"scwx::gr::placefile"}; +static const auto logger_ = util::Logger::Create(logPrefix_); + +enum class DrawingStatement +{ + Standard, + Line, + Triangles, + Image, + Polygon +}; + +class Placefile::Impl +{ +public: + explicit Impl() = default; + ~Impl() = default; + + struct Object + { + double x_ {}; + double y_ {}; + }; + + struct DrawItem + { + }; + + struct PlaceDrawItem : DrawItem + { + boost::units::quantity threshold_ {}; + boost::gil::rgba8_pixel_t color_ {}; + double latitude_ {}; + double longitude_ {}; + double x_ {}; + double y_ {}; + std::string text_ {}; + }; + + void ParseLocation(const std::string& latitudeToken, + const std::string& longitudeToken, + double& latitude, + double& longitude, + double& x, + double& y); + void ProcessLine(const std::string& line, + const std::vector& tokenList); + + std::chrono::seconds refresh_ {-1}; + + // Parsing state + boost::units::quantity threshold_ { + 999.0 * boost::units::metric::nautical_mile_base_unit::unit_type()}; + boost::gil::rgba8_pixel_t color_ {255, 255, 255, 255}; + ColorMode colorMode_ {ColorMode::RGBA}; + std::vector objectStack_ {}; + DrawingStatement currentStatement_ {DrawingStatement::Standard}; + + // References + std::unordered_map iconFiles_ {}; + std::unordered_map fonts_ {}; + + std::vector> drawItems_ {}; +}; + +Placefile::Placefile() : p(std::make_unique()) {} +Placefile::~Placefile() = default; + +Placefile::Placefile(Placefile&&) noexcept = default; +Placefile& Placefile::operator=(Placefile&&) noexcept = default; + +bool Placefile::IsValid() const +{ + return p->drawItems_.size() > 0; +} + +std::shared_ptr Placefile::Load(const std::string& filename) +{ + logger_->debug("Loading placefile: {}", filename); + std::ifstream f(filename, std::ios_base::in); + return Load(f); +} + +std::shared_ptr Placefile::Load(std::istream& is) +{ + std::shared_ptr placefile = std::make_shared(); + + std::string line; + while (scwx::util::getline(is, line)) + { + // Find position of comment (;) + std::size_t lineEnd = line.find(';'); + if (lineEnd == std::string::npos) + { + // Remove comment + line.erase(lineEnd); + } + + // Remove extra spacing from line + boost::trim(line); + + boost::char_separator delimiter(", "); + boost::tokenizer tokens(line, delimiter); + std::vector tokenList; + + for (auto& token : tokens) + { + tokenList.push_back(token); + } + + if (tokenList.size() >= 1) + { + try + { + switch (placefile->p->currentStatement_) + { + case DrawingStatement::Standard: + placefile->p->ProcessLine(line, tokenList); + break; + + case DrawingStatement::Line: + case DrawingStatement::Triangles: + case DrawingStatement::Image: + case DrawingStatement::Polygon: + if (boost::iequals(tokenList[0], "End:")) + { + placefile->p->currentStatement_ = DrawingStatement::Standard; + } + break; + } + } + catch (const std::exception&) + { + logger_->warn("Could not parse line: {}", line); + } + } + } + + return placefile; +} + +void Placefile::Impl::ProcessLine(const std::string& line, + const std::vector& tokenList) +{ + currentStatement_ = DrawingStatement::Standard; + + if (boost::iequals(tokenList[0], "Threshold:")) + { + // Threshold: nautical_miles + if (tokenList.size() >= 2) + { + threshold_ = + static_cast>( + std::stod(tokenList[1]) * + boost::units::metric::nautical_mile_base_unit::unit_type()); + } + } + else if (boost::iequals(tokenList[0], "HSLuv:")) + { + // HSLuv: value + if (tokenList.size() >= 2) + { + if (boost::iequals(tokenList[1], "true")) + { + colorMode_ = ColorMode::HSLuv; + } + else + { + colorMode_ = ColorMode::RGBA; + } + } + } + else if (boost::iequals(tokenList[0], "Color:")) + { + // Color: red green blue + if (tokenList.size() >= 2) + { + color_ = ParseColor(tokenList, 1, colorMode_); + } + } + else if (boost::iequals(tokenList[0], "Refresh:")) + { + // Refresh: minutes + if (tokenList.size() >= 2) + { + refresh_ = std::chrono::minutes {std::stoi(tokenList[1])}; + } + } + else if (boost::iequals(tokenList[0], "RefreshSeconds:")) + { + // RefreshSeconds: seconds + if (tokenList.size() >= 2) + { + refresh_ = std::chrono::seconds {std::stoi(tokenList[1])}; + } + } + else if (boost::iequals(tokenList[0], "Place:")) + { + // Place: latitude, longitude, string with spaces + std::regex re {"Place:\\s*([+\\-0-9\\.]+),\\s*([+\\-0-9\\.]+),\\s*(.+)"}; + std::smatch match; + std::regex_match(line, match, re); + + if (match.size() >= 4) + { + std::shared_ptr di = std::make_shared(); + + di->threshold_ = threshold_; + di->color_ = color_; + + ParseLocation(match[1].str(), + match[2].str(), + di->latitude_, + di->longitude_, + di->x_, + di->y_); + + di->text_ = match[3].str(); + + drawItems_.emplace_back(std::move(di)); + } + else + { + logger_->warn("Place statement malformed: {}", line); + } + } + else if (boost::iequals(tokenList[0], "IconFile:")) + { + // IconFile: fileNumber, iconWidth, iconHeight, hotX, hotY, fileName + + // TODO + } + else if (boost::iequals(tokenList[0], "Icon:")) + { + // Icon: lat, lon, angle, fileNumber, iconNumber, hoverText + + // TODO + } + else if (boost::iequals(tokenList[0], "Font:")) + { + // Font: fontNumber, pixels, flags, "face" + + // TODO + } + else if (boost::iequals(tokenList[0], "Text:")) + { + // Text: lat, lon, fontNumber, "string", "hover" + + // TODO + } + else if (boost::iequals(tokenList[0], "Object:")) + { + // Object: lat, lon + // ... + // End: + std::regex re {"Object:\\s*([+\\-0-9\\.]+),\\s*([+\\-0-9\\.]+)"}; + std::smatch match; + std::regex_match(line, match, re); + + double latitude {}; + double longitude {}; + + if (match.size() >= 3) + { + latitude = std::stod(match[1].str()); + longitude = std::stod(match[2].str()); + } + else + { + logger_->warn("Object statement malformed: {}", line); + } + + objectStack_.emplace_back(Object {latitude, longitude}); + } + else if (boost::iequals(tokenList[0], "End:")) + { + // Object End + if (!objectStack_.empty()) + { + objectStack_.pop_back(); + } + else + { + logger_->warn("End found without Object"); + } + } + else if (boost::iequals(tokenList[0], "Line:")) + { + // Line: width, flags [, hover_text] + // lat, lon + // ... + // End: + currentStatement_ = DrawingStatement::Line; + + // TODO + } + else if (boost::iequals(tokenList[0], "Triangles:")) + { + // Triangles: + // lat, lon [, r, g, b [,a]] + // ... + // End: + currentStatement_ = DrawingStatement::Triangles; + + // TODO + } + else if (boost::iequals(tokenList[0], "Image:")) + { + // Image: image_file + // lat, lon, Tu [, Tv ] + // ... + // End: + currentStatement_ = DrawingStatement::Image; + + // TODO + } + else if (boost::iequals(tokenList[0], "Polygon:")) + { + // Polygon: + // lat1, lon1 [, r, g, b [,a]] ; start of the first contour + // ... + // lat1, lon1 ; repeating the first point closes the + // ; contour + // + // lat2, lon2 ; next point starts a new contour + // ... + // lat2, lon2 ; and repeating it ends the contour + // End: + currentStatement_ = DrawingStatement::Polygon; + + // TODO + } +} + +void Placefile::Impl::ParseLocation(const std::string& latitudeToken, + const std::string& longitudeToken, + double& latitude, + double& longitude, + double& x, + double& y) +{ + if (objectStack_.empty()) + { + // If an Object statement is not currently open, parse latitude and + // longitude tokens as-is + latitude = std::stod(latitudeToken); + longitude = std::stod(longitudeToken); + } + else + { + // If an Object statement is open, the latitude and longitude are from the + // outermost Object + latitude = objectStack_[0].x_; + longitude = objectStack_[0].y_; + + // The latitude and longitude tokens are interpreted as x, y offsets + x = std::stod(latitudeToken); + y = std::stod(longitudeToken); + + // If there are inner Object statements open, treat these as x, y offsets + for (std::size_t i = 1; i < objectStack_.size(); i++) + { + x += objectStack_[i].x_; + y += objectStack_[i].y_; + } + } +} + +} // namespace gr +} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index b6c4fb1c..3f7ab8d9 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -45,6 +45,11 @@ set(SRC_COMMON source/scwx/common/characters.cpp source/scwx/common/products.cpp source/scwx/common/sites.cpp source/scwx/common/vcp.cpp) +set(HDR_GR include/scwx/gr/color.hpp + include/scwx/gr/gr_types.hpp + include/scwx/gr/placefile.hpp) +set(SRC_GR source/scwx/gr/color.cpp + source/scwx/gr/placefile.cpp) set(HDR_NETWORK include/scwx/network/dir_list.hpp) set(SRC_NETWORK source/scwx/network/dir_list.cpp) set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp @@ -195,6 +200,8 @@ add_library(wxdata OBJECT ${HDR_AWIPS} ${SRC_AWIPS} ${HDR_COMMON} ${SRC_COMMON} + ${HDR_GR} + ${SRC_GR} ${HDR_NETWORK} ${SRC_NETWORK} ${HDR_PROVIDER} @@ -213,6 +220,8 @@ source_group("Header Files\\awips" FILES ${HDR_AWIPS}) source_group("Source Files\\awips" FILES ${SRC_AWIPS}) source_group("Header Files\\common" FILES ${HDR_COMMON}) source_group("Source Files\\common" FILES ${SRC_COMMON}) +source_group("Header Files\\gr" FILES ${HDR_GR}) +source_group("Source Files\\gr" FILES ${SRC_GR}) source_group("Header Files\\network" FILES ${HDR_NETWORK}) source_group("Source Files\\network" FILES ${SRC_NETWORK}) source_group("Header Files\\provider" FILES ${HDR_PROVIDER})