#include #include #include #include #include #include #include #include #include using namespace units::literals; 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_ {}; }; void ParseLocation(const std::string& latitudeToken, const std::string& longitudeToken, double& latitude, double& longitude, double& x, double& y); void ProcessElement(const std::string& line); void ProcessElementEnd(); void ProcessLine(const std::string& line); static void ProcessEscapeCharacters(std::string& s); static void TrimQuotes(std::string& s); std::string name_ {}; std::string title_ {}; std::chrono::seconds refresh_ {-1}; // Parsing state units::length::nautical_miles threshold_ {999.0_nmi}; boost::gil::rgba8_pixel_t color_ {255, 255, 255, 255}; ColorMode colorMode_ {ColorMode::RGBA}; std::vector objectStack_ {}; DrawingStatement currentStatement_ {DrawingStatement::Standard}; std::shared_ptr currentDrawItem_ {nullptr}; std::vector currentPolygonContour_ {}; // 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::vector> Placefile::GetDrawItems() { return p->drawItems_; } std::vector> Placefile::icon_files() { std::vector> iconFiles {}; iconFiles.reserve(p->iconFiles_.size()); std::transform(p->iconFiles_.begin(), p->iconFiles_.end(), std::back_inserter(iconFiles), [](auto& iconFile) { return iconFile.second; }); return iconFiles; } std::string Placefile::name() const { return p->name_; } std::string Placefile::title() const { return p->title_; } std::unordered_map> Placefile::fonts() { return p->fonts_; } std::shared_ptr Placefile::font(std::size_t i) { auto it = p->fonts_.find(i); if (it != p->fonts_.cend()) { return it->second; } return nullptr; } std::shared_ptr Placefile::Load(const std::string& filename) { logger_->debug("Loading placefile: {}", filename); std::ifstream f(filename, std::ios_base::in); return Load(filename, f); } std::shared_ptr Placefile::Load(const std::string& name, std::istream& is) { std::shared_ptr placefile = std::make_shared(); placefile->p->name_ = name; std::string line; while (scwx::util::getline(is, line)) { // Find position of comment (;) bool inQuotes = false; for (std::size_t i = 0; i < line.size(); ++i) { if (!inQuotes && line[i] == ';') { // Remove comment line.erase(i); break; } else if (line[i] == '"') { // Toggle quote state inQuotes = !inQuotes; } } // Remove extra spacing from line boost::trim(line); if (line.size() >= 1) { try { switch (placefile->p->currentStatement_) { case DrawingStatement::Standard: placefile->p->ProcessLine(line); break; case DrawingStatement::Line: case DrawingStatement::Triangles: case DrawingStatement::Image: case DrawingStatement::Polygon: if (boost::istarts_with(line, "End:")) { placefile->p->ProcessElementEnd(); placefile->p->currentStatement_ = DrawingStatement::Standard; placefile->p->currentDrawItem_ = nullptr; } else if (placefile->p->currentDrawItem_ != nullptr) { placefile->p->ProcessElement(line); } break; } } catch (const std::exception&) { logger_->warn("Could not parse line: {}", line); } } } return placefile; } void Placefile::Impl::ProcessLine(const std::string& line) { static const std::string titleKey_ {"Title:"}; static const std::string thresholdKey_ {"Threshold:"}; static const std::string timeRangeKey_ {"TimeRange:"}; static const std::string hsluvKey_ {"HSLuv:"}; static const std::string colorKey_ {"Color:"}; static const std::string refreshKey_ {"Refresh:"}; static const std::string refreshSecondsKey_ {"RefreshSeconds:"}; static const std::string placeKey_ {"Place:"}; static const std::string iconFileKey_ {"IconFile:"}; static const std::string iconKey_ {"Icon:"}; static const std::string fontKey_ {"Font:"}; static const std::string textKey_ {"Text:"}; static const std::string objectKey_ {"Object:"}; static const std::string endKey_ {"End:"}; static const std::string lineKey_ {"Line:"}; static const std::string trianglesKey_ {"Triangles:"}; static const std::string imageKey_ {"Image:"}; static const std::string polygonKey_ {"Polygon:"}; currentStatement_ = DrawingStatement::Standard; // When tokenizing, add one additional delimiter to discard unexpected // parameters (where appropriate) if (boost::istarts_with(line, titleKey_)) { // Title: title title_ = line.substr(titleKey_.size()); boost::trim(title_); } else if (boost::istarts_with(line, thresholdKey_)) { // Threshold: nautical_miles std::vector tokenList = util::ParseTokens(line, {" "}, thresholdKey_.size()); if (tokenList.size() >= 1) { threshold_ = units::length::nautical_miles(std::stod(tokenList[0])); } } else if (boost::istarts_with(line, timeRangeKey_)) { // TimeRange: start_time end_time // TODO } else if (boost::istarts_with(line, hsluvKey_)) { // HSLuv: value std::vector tokenList = util::ParseTokens(line, {" "}, hsluvKey_.size()); if (tokenList.size() >= 1) { if (boost::iequals(tokenList[0], "true")) { colorMode_ = ColorMode::HSLuv; } else { colorMode_ = ColorMode::RGBA; } } } else if (boost::istarts_with(line, colorKey_)) { // Color: red green blue [alpha] std::vector tokenList = util::ParseTokens(line, {" ", " ", " ", " "}, colorKey_.size()); if (tokenList.size() >= 3) { color_ = ParseColor(tokenList, 0, colorMode_); } } else if (boost::istarts_with(line, refreshKey_)) { // Refresh: minutes std::vector tokenList = util::ParseTokens(line, {" "}, refreshKey_.size()); if (tokenList.size() >= 1) { refresh_ = std::chrono::minutes {std::stoi(tokenList[0])}; } } else if (boost::istarts_with(line, refreshSecondsKey_)) { // RefreshSeconds: seconds std::vector tokenList = util::ParseTokens(line, {" "}, refreshSecondsKey_.size()); if (tokenList.size() >= 1) { refresh_ = std::chrono::seconds {std::stoi(tokenList[0])}; } } else if (boost::istarts_with(line, placeKey_)) { // Place: latitude, longitude, string with spaces std::vector tokenList = util::ParseTokens(line, {",", ","}, placeKey_.size()); if (tokenList.size() >= 3) { std::shared_ptr di = std::make_shared(); di->threshold_ = threshold_; di->color_ = color_; ParseLocation(tokenList[0], tokenList[1], di->latitude_, di->longitude_, di->x_, di->y_); ProcessEscapeCharacters(tokenList[2]); di->text_.swap(tokenList[2]); drawItems_.emplace_back(std::move(di)); } else { logger_->warn("Place statement malformed: {}", line); } } else if (boost::istarts_with(line, iconFileKey_)) { // IconFile: fileNumber, iconWidth, iconHeight, hotX, hotY, fileName std::vector tokenList = util::ParseTokens( line, {",", ",", ",", ",", ","}, iconFileKey_.size()); if (tokenList.size() >= 6) { std::shared_ptr iconFile = std::make_shared(); iconFile->fileNumber_ = std::stoul(tokenList[0]); iconFile->iconWidth_ = std::stoul(tokenList[1]); iconFile->iconHeight_ = std::stoul(tokenList[2]); iconFile->hotX_ = std::stoul(tokenList[3]); iconFile->hotY_ = std::stoul(tokenList[4]); TrimQuotes(tokenList[5]); iconFile->filename_.swap(tokenList[5]); iconFiles_.insert_or_assign(iconFile->fileNumber_, iconFile); } else { logger_->warn("IconFile statement malformed: {}", line); } } else if (boost::istarts_with(line, iconKey_)) { // Icon: lat, lon, angle, fileNumber, iconNumber, hoverText std::vector tokenList = util::ParseTokens(line, {",", ",", ",", ",", ","}, iconKey_.size()); std::shared_ptr di = nullptr; if (tokenList.size() >= 5) { di = std::make_shared(); di->threshold_ = threshold_; ParseLocation(tokenList[0], tokenList[1], di->latitude_, di->longitude_, di->x_, di->y_); di->angle_ = units::angle::degrees(std::stod(tokenList[2])); di->fileNumber_ = std::stoul(tokenList[3]); di->iconNumber_ = std::stoul(tokenList[4]); } if (tokenList.size() >= 6) { ProcessEscapeCharacters(tokenList[5]); TrimQuotes(tokenList[5]); di->hoverText_.swap(tokenList[5]); } if (di != nullptr) { drawItems_.emplace_back(std::move(di)); } else { logger_->warn("Icon statement malformed: {}", line); } } else if (boost::istarts_with(line, fontKey_)) { // Font: fontNumber, pixels, flags, "face" std::vector tokenList = util::ParseTokens(line, {",", ",", ",", ","}, fontKey_.size()); if (tokenList.size() >= 4) { std::shared_ptr font = std::make_shared(); font->fontNumber_ = std::stoul(tokenList[0]); font->pixels_ = std::stoul(tokenList[1]); font->flags_ = std::stoi(tokenList[2]); TrimQuotes(tokenList[3]); font->face_.swap(tokenList[3]); fonts_.insert_or_assign(font->fontNumber_, font); } else { logger_->warn("Font statement malformed: {}", line); } } else if (boost::istarts_with(line, textKey_)) { // Text: lat, lon, fontNumber, "string", "hover" std::vector tokenList = util::ParseTokens(line, {",", ",", ",", ",", ","}, textKey_.size()); std::shared_ptr di = nullptr; if (tokenList.size() >= 4) { di = std::make_shared(); di->threshold_ = threshold_; di->color_ = color_; ParseLocation(tokenList[0], tokenList[1], di->latitude_, di->longitude_, di->x_, di->y_); di->fontNumber_ = std::stoul(tokenList[2]); ProcessEscapeCharacters(tokenList[3]); TrimQuotes(tokenList[3]); di->text_.swap(tokenList[3]); } if (tokenList.size() >= 5) { ProcessEscapeCharacters(tokenList[4]); TrimQuotes(tokenList[4]); di->hoverText_.swap(tokenList[4]); } if (di != nullptr) { drawItems_.emplace_back(std::move(di)); } else { logger_->warn("Text statement malformed: {}", line); } } else if (boost::istarts_with(line, objectKey_)) { // Object: lat, lon // ... // End: std::vector tokenList = util::ParseTokens(line, {",", ","}, objectKey_.size()); double latitude {}; double longitude {}; if (tokenList.size() >= 2) { latitude = std::stod(tokenList[0]); longitude = std::stod(tokenList[1]); } else { logger_->warn("Object statement malformed: {}", line); } objectStack_.emplace_back(Object {latitude, longitude}); } else if (boost::istarts_with(line, endKey_)) { // Object End if (!objectStack_.empty()) { objectStack_.pop_back(); } } else if (boost::istarts_with(line, lineKey_)) { // Line: width, flags [, hover_text] // lat, lon // ... // End: std::vector tokenList = util::ParseTokens(line, {",", ","}, lineKey_.size()); currentStatement_ = DrawingStatement::Line; std::shared_ptr di = nullptr; if (tokenList.size() >= 2) { di = std::make_shared(); di->threshold_ = threshold_; di->color_ = color_; di->width_ = std::stoul(tokenList[0]); di->flags_ = std::stoul(tokenList[1]); } if (tokenList.size() >= 3) { ProcessEscapeCharacters(tokenList[2]); TrimQuotes(tokenList[2]); di->hoverText_.swap(tokenList[2]); } if (di != nullptr) { currentDrawItem_ = di; drawItems_.emplace_back(std::move(di)); } else { logger_->warn("Line statement malformed: {}", line); } } else if (boost::istarts_with(line, trianglesKey_)) { // Triangles: // lat, lon [, r, g, b [,a]] // ... // End: currentStatement_ = DrawingStatement::Triangles; std::shared_ptr di = std::make_shared(); di->threshold_ = threshold_; di->color_ = color_; currentDrawItem_ = di; drawItems_.emplace_back(std::move(di)); } else if (boost::istarts_with(line, imageKey_)) { // Image: image_file // lat, lon, Tu [, Tv ] // ... // End: currentStatement_ = DrawingStatement::Image; // TODO } else if (boost::istarts_with(line, polygonKey_)) { // 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; std::shared_ptr di = std::make_shared(); di->threshold_ = threshold_; di->color_ = color_; currentDrawItem_ = di; drawItems_.emplace_back(std::move(di)); } else { logger_->warn("Unknown statement: {}", line); } } void Placefile::Impl::ProcessElement(const std::string& line) { if (currentStatement_ == DrawingStatement::Line) { // Line: width, flags [, hover_text] // lat, lon // ... // End: std::vector tokenList = util::ParseTokens(line, {",", ","}); if (tokenList.size() >= 2) { LineDrawItem::Element element; ParseLocation(tokenList[0], tokenList[1], element.latitude_, element.longitude_, element.x_, element.y_); std::static_pointer_cast(currentDrawItem_) ->elements_.emplace_back(std::move(element)); } else { logger_->warn("Line sub-statement malformed: {}", line); } } else if (currentStatement_ == DrawingStatement::Triangles) { // Triangles: // lat, lon [, r, g, b [,a]] // ... // End: std::vector tokenList = util::ParseTokens(line, {",", ",", ",", ",", ",", ","}); TrianglesDrawItem::Element element; if (tokenList.size() >= 5) { element.color_ = ParseColor(tokenList, 2, colorMode_); } if (tokenList.size() >= 2) { ParseLocation(tokenList[0], tokenList[1], element.latitude_, element.longitude_, element.x_, element.y_); std::static_pointer_cast(currentDrawItem_) ->elements_.emplace_back(std::move(element)); } else { logger_->warn("Triangles sub-statement malformed: {}", line); } } else if (currentStatement_ == DrawingStatement::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: std::vector tokenList = util::ParseTokens(line, {",", ",", ",", ",", ",", ","}); PolygonDrawItem::Element element; if (tokenList.size() >= 5) { element.color_ = ParseColor(tokenList, 2, colorMode_); } if (tokenList.size() >= 2) { ParseLocation(tokenList[0], tokenList[1], element.latitude_, element.longitude_, element.x_, element.y_); currentPolygonContour_.emplace_back(std::move(element)); if (currentPolygonContour_.size() >= 2) { auto& first = currentPolygonContour_.front(); auto& last = currentPolygonContour_.back(); // Repeating the first point closes the contour if (first.latitude_ == last.latitude_ && first.longitude_ == last.longitude_ && // first.x_ == last.x_ && // first.y_ == last.y_) { auto& contours = std::static_pointer_cast(currentDrawItem_) ->contours_; auto& newContour = contours.emplace_back( std::vector {}); newContour.swap(currentPolygonContour_); } } } else { logger_->warn("Polygon sub-statement malformed: {}", line); } } } void Placefile::Impl::ProcessElementEnd() { if (currentStatement_ == DrawingStatement::Polygon) { auto di = std::static_pointer_cast(currentDrawItem_); // Complete the current contour when ending the Polygon statement if (!currentPolygonContour_.empty()) { auto& contours = di->contours_; auto& newContour = contours.emplace_back(std::vector {}); newContour.swap(currentPolygonContour_); } if (!di->contours_.empty()) { std::vector coordinates {}; std::transform(di->contours_[0].cbegin(), di->contours_[0].cend(), std::back_inserter(coordinates), [](auto& element) { return common::Coordinate {element.latitude_, element.longitude_}; }); di->center_ = GetCentroid(coordinates); } } } 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_; } } } void Placefile::Impl::ProcessEscapeCharacters(std::string& s) { boost::replace_all(s, "\\n", "\n"); } void Placefile::Impl::TrimQuotes(std::string& s) { if (s.size() >= 2 && s.front() == '"' && s.back() == '"') { s.erase(s.size() - 1); s.erase(0, 1); } } } // namespace gr } // namespace scwx