mirror of
https://github.com/ciphervance/supercell-wx.git
synced 2025-10-30 20:10:06 +00:00
468 lines
14 KiB
C++
468 lines
14 KiB
C++
#define NOMINMAX
|
|
|
|
#include <scwx/qt/model/alert_model.hpp>
|
|
#include <scwx/qt/config/county_database.hpp>
|
|
#include <scwx/qt/manager/text_event_manager.hpp>
|
|
#include <scwx/qt/types/qt_types.hpp>
|
|
#include <scwx/qt/util/geographic_lib.hpp>
|
|
#include <scwx/common/geographic.hpp>
|
|
#include <scwx/util/logger.hpp>
|
|
#include <scwx/util/strings.hpp>
|
|
#include <scwx/util/time.hpp>
|
|
|
|
#include <format>
|
|
|
|
#include <QApplication>
|
|
#include <QFontMetrics>
|
|
|
|
namespace scwx
|
|
{
|
|
namespace qt
|
|
{
|
|
namespace model
|
|
{
|
|
|
|
static const std::string logPrefix_ = "scwx::qt::model::alert_model";
|
|
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
|
|
|
|
static constexpr int kFirstColumn = static_cast<int>(AlertModel::Column::Etn);
|
|
static constexpr int kLastColumn =
|
|
static_cast<int>(AlertModel::Column::Distance);
|
|
static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1;
|
|
|
|
class AlertModelImpl
|
|
{
|
|
public:
|
|
explicit AlertModelImpl();
|
|
~AlertModelImpl() = default;
|
|
|
|
static std::string GetCounties(const types::TextEventKey& key);
|
|
static std::string GetState(const types::TextEventKey& key);
|
|
static std::chrono::system_clock::time_point
|
|
GetStartTime(const types::TextEventKey& key);
|
|
static std::string GetStartTimeString(const types::TextEventKey& key);
|
|
static std::chrono::system_clock::time_point
|
|
GetEndTime(const types::TextEventKey& key);
|
|
static std::string GetEndTimeString(const types::TextEventKey& key);
|
|
|
|
std::shared_ptr<manager::TextEventManager> textEventManager_;
|
|
|
|
QList<types::TextEventKey> textEventKeys_;
|
|
|
|
const GeographicLib::Geodesic& geodesic_;
|
|
|
|
std::unordered_map<types::TextEventKey,
|
|
common::Coordinate,
|
|
types::TextEventHash<types::TextEventKey>>
|
|
centroidMap_;
|
|
std::unordered_map<types::TextEventKey,
|
|
double,
|
|
types::TextEventHash<types::TextEventKey>>
|
|
distanceMap_;
|
|
scwx::common::DistanceType distanceDisplay_;
|
|
scwx::common::Coordinate previousPosition_;
|
|
};
|
|
|
|
AlertModel::AlertModel(QObject* parent) :
|
|
QAbstractTableModel(parent), p(std::make_unique<AlertModelImpl>())
|
|
{
|
|
}
|
|
AlertModel::~AlertModel() = default;
|
|
|
|
types::TextEventKey AlertModel::key(const QModelIndex& index) const
|
|
{
|
|
return index.isValid() ? p->textEventKeys_[index.row()] :
|
|
types::TextEventKey {};
|
|
}
|
|
|
|
common::Coordinate AlertModel::centroid(const types::TextEventKey& key) const
|
|
{
|
|
common::Coordinate centroid {};
|
|
|
|
const auto& it = p->centroidMap_.find(key);
|
|
if (it != p->centroidMap_.cend())
|
|
{
|
|
centroid = it->second;
|
|
}
|
|
|
|
return centroid;
|
|
}
|
|
|
|
int AlertModel::rowCount(const QModelIndex& parent) const
|
|
{
|
|
return parent.isValid() ? 0 : p->textEventKeys_.size();
|
|
}
|
|
|
|
int AlertModel::columnCount(const QModelIndex& parent) const
|
|
{
|
|
return parent.isValid() ? 0 : kNumColumns;
|
|
}
|
|
|
|
QVariant AlertModel::data(const QModelIndex& index, int role) const
|
|
{
|
|
if (!index.isValid() || index.row() < 0 ||
|
|
index.row() >= p->textEventKeys_.size())
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
const auto& textEventKey = p->textEventKeys_.at(index.row());
|
|
|
|
if (role == Qt::ItemDataRole::DisplayRole ||
|
|
role == types::ItemDataRole::SortRole)
|
|
{
|
|
switch (index.column())
|
|
{
|
|
case static_cast<int>(Column::Etn):
|
|
return textEventKey.etn_;
|
|
|
|
case static_cast<int>(Column::OfficeId):
|
|
return QString::fromStdString(textEventKey.officeId_);
|
|
|
|
case static_cast<int>(Column::Phenomenon):
|
|
return QString::fromStdString(
|
|
awips::GetPhenomenonText(textEventKey.phenomenon_));
|
|
|
|
case static_cast<int>(Column::Significance):
|
|
return QString::fromStdString(
|
|
awips::GetSignificanceText(textEventKey.significance_));
|
|
|
|
case static_cast<int>(Column::State):
|
|
return QString::fromStdString(AlertModelImpl::GetState(textEventKey));
|
|
|
|
case static_cast<int>(Column::Counties):
|
|
return QString::fromStdString(
|
|
AlertModelImpl::GetCounties(textEventKey));
|
|
|
|
case static_cast<int>(Column::StartTime):
|
|
return QString::fromStdString(
|
|
AlertModelImpl::GetStartTimeString(textEventKey));
|
|
|
|
case static_cast<int>(Column::EndTime):
|
|
return QString::fromStdString(
|
|
AlertModelImpl::GetEndTimeString(textEventKey));
|
|
|
|
case static_cast<int>(Column::Distance):
|
|
if (role == Qt::DisplayRole)
|
|
{
|
|
if (p->distanceDisplay_ == scwx::common::DistanceType::Miles)
|
|
{
|
|
return QString("%1 mi").arg(
|
|
static_cast<uint32_t>(p->distanceMap_.at(textEventKey) *
|
|
scwx::common::kMilesPerMeter));
|
|
}
|
|
else
|
|
{
|
|
return QString("%1 km").arg(
|
|
static_cast<uint32_t>(p->distanceMap_.at(textEventKey) *
|
|
scwx::common::kKilometersPerMeter));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return p->distanceMap_.at(textEventKey);
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if (role == types::ItemDataRole::TimePointRole)
|
|
{
|
|
switch (index.column())
|
|
{
|
|
case static_cast<int>(Column::StartTime):
|
|
return QVariant::fromValue(AlertModelImpl::GetStartTime(textEventKey));
|
|
|
|
case static_cast<int>(Column::EndTime):
|
|
return QVariant::fromValue(AlertModelImpl::GetEndTime(textEventKey));
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
QVariant
|
|
AlertModel::headerData(int section, Qt::Orientation orientation, int role) const
|
|
{
|
|
if (role == Qt::DisplayRole)
|
|
{
|
|
if (orientation == Qt::Horizontal)
|
|
{
|
|
switch (section)
|
|
{
|
|
case static_cast<int>(Column::Etn):
|
|
return tr("ETN");
|
|
|
|
case static_cast<int>(Column::OfficeId):
|
|
return tr("Office ID");
|
|
|
|
case static_cast<int>(Column::Phenomenon):
|
|
return tr("Phenomenon");
|
|
|
|
case static_cast<int>(Column::Significance):
|
|
return tr("Significance");
|
|
|
|
case static_cast<int>(Column::State):
|
|
return tr("State");
|
|
|
|
case static_cast<int>(Column::Counties):
|
|
return tr("Counties / Areas");
|
|
|
|
case static_cast<int>(Column::StartTime):
|
|
return tr("Start Time");
|
|
|
|
case static_cast<int>(Column::EndTime):
|
|
return tr("End Time");
|
|
|
|
case static_cast<int>(Column::Distance):
|
|
return tr("Distance");
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if (role == Qt::ItemDataRole::SizeHintRole)
|
|
{
|
|
static const QFontMetrics fontMetrics(QApplication::font());
|
|
|
|
QSize contentsSize {};
|
|
|
|
switch (section)
|
|
{
|
|
case static_cast<int>(Column::Etn):
|
|
contentsSize = fontMetrics.size(0, "0000"); // 24x16
|
|
break;
|
|
|
|
case static_cast<int>(Column::Phenomenon):
|
|
contentsSize = fontMetrics.size(0, QString(10, 'W'));
|
|
break;
|
|
|
|
case static_cast<int>(Column::State):
|
|
contentsSize = fontMetrics.size(0, "WW, WW");
|
|
break;
|
|
|
|
case static_cast<int>(Column::Counties):
|
|
contentsSize = fontMetrics.size(0, QString(15, 'W'));
|
|
break;
|
|
|
|
case static_cast<int>(Column::StartTime):
|
|
case static_cast<int>(Column::EndTime):
|
|
contentsSize = fontMetrics.size(0, "0000-00-00 00:00:00");
|
|
break;
|
|
|
|
case static_cast<int>(Column::Distance):
|
|
contentsSize = fontMetrics.size(0, "00000 km");
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (contentsSize != QSize {})
|
|
{
|
|
// Add padding
|
|
// TODO: Ideally, derived using the current style, but how?
|
|
contentsSize += QSize(12, 8);
|
|
return QVariant(contentsSize);
|
|
}
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
void AlertModel::HandleAlert(const types::TextEventKey& alertKey,
|
|
size_t messageIndex)
|
|
{
|
|
logger_->trace("Handle alert: {}", alertKey.ToString());
|
|
|
|
double distanceInMeters;
|
|
|
|
// Get the most recent segment for the event
|
|
auto alertMessages = p->textEventManager_->message_list(alertKey);
|
|
std::shared_ptr<const awips::Segment> alertSegment =
|
|
alertMessages[messageIndex]->segments().back();
|
|
|
|
if (alertSegment->codedLocation_.has_value())
|
|
{
|
|
// Update centroid and distance
|
|
common::Coordinate centroid =
|
|
common::GetCentroid(alertSegment->codedLocation_->coordinates());
|
|
|
|
p->geodesic_.Inverse(p->previousPosition_.latitude_,
|
|
p->previousPosition_.longitude_,
|
|
centroid.latitude_,
|
|
centroid.longitude_,
|
|
distanceInMeters);
|
|
|
|
p->centroidMap_.insert_or_assign(alertKey, centroid);
|
|
p->distanceMap_.insert_or_assign(alertKey, distanceInMeters);
|
|
}
|
|
else if (!p->centroidMap_.contains(alertKey))
|
|
{
|
|
// The alert has no location, so provide a default
|
|
p->centroidMap_.insert_or_assign(alertKey, common::Coordinate {0.0, 0.0});
|
|
p->distanceMap_.insert_or_assign(alertKey, 0.0);
|
|
}
|
|
|
|
// Update row
|
|
if (!p->textEventKeys_.contains(alertKey))
|
|
{
|
|
int newIndex = p->textEventKeys_.size();
|
|
beginInsertRows(QModelIndex(), newIndex, newIndex);
|
|
p->textEventKeys_.push_back(alertKey);
|
|
endInsertRows();
|
|
}
|
|
else
|
|
{
|
|
const int row = p->textEventKeys_.indexOf(alertKey);
|
|
QModelIndex topLeft = createIndex(row, kFirstColumn);
|
|
QModelIndex bottomRight = createIndex(row, kLastColumn);
|
|
|
|
Q_EMIT dataChanged(topLeft, bottomRight);
|
|
}
|
|
}
|
|
|
|
void AlertModel::HandleMapUpdate(double latitude, double longitude)
|
|
{
|
|
logger_->trace("Handle map update: {}, {}", latitude, longitude);
|
|
|
|
double distanceInMeters;
|
|
|
|
for (const auto& textEvent : p->textEventKeys_)
|
|
{
|
|
auto& centroid = p->centroidMap_.at(textEvent);
|
|
|
|
if (centroid != common::Coordinate {0.0, 0.0})
|
|
{
|
|
p->geodesic_.Inverse(latitude,
|
|
longitude,
|
|
centroid.latitude_,
|
|
centroid.longitude_,
|
|
distanceInMeters);
|
|
p->distanceMap_.insert_or_assign(textEvent, distanceInMeters);
|
|
}
|
|
}
|
|
|
|
p->previousPosition_ = {latitude, longitude};
|
|
|
|
QModelIndex topLeft = createIndex(0, static_cast<int>(Column::Distance));
|
|
QModelIndex bottomRight =
|
|
createIndex(rowCount() - 1, static_cast<int>(Column::Distance));
|
|
|
|
Q_EMIT dataChanged(topLeft, bottomRight);
|
|
}
|
|
|
|
AlertModelImpl::AlertModelImpl() :
|
|
textEventManager_ {manager::TextEventManager::Instance()},
|
|
textEventKeys_ {},
|
|
geodesic_(util::GeographicLib::DefaultGeodesic()),
|
|
distanceMap_ {},
|
|
distanceDisplay_ {scwx::common::DistanceType::Miles},
|
|
previousPosition_ {}
|
|
{
|
|
}
|
|
|
|
std::string AlertModelImpl::GetCounties(const types::TextEventKey& key)
|
|
{
|
|
auto messageList = manager::TextEventManager::Instance()->message_list(key);
|
|
|
|
if (messageList.size() > 0)
|
|
{
|
|
auto& lastMessage = messageList.back();
|
|
size_t segmentCount = lastMessage->segment_count();
|
|
auto lastSegment = lastMessage->segment(segmentCount - 1);
|
|
auto fipsIds = lastSegment->header_->ugc_.fips_ids();
|
|
|
|
std::vector<std::string> counties;
|
|
counties.reserve(fipsIds.size());
|
|
for (auto& id : fipsIds)
|
|
{
|
|
counties.push_back(config::CountyDatabase::GetCountyName(id));
|
|
}
|
|
std::sort(counties.begin(), counties.end());
|
|
|
|
return scwx::util::ToString(counties);
|
|
}
|
|
else
|
|
{
|
|
logger_->warn("GetCounties(): No message associated with key: {}",
|
|
key.ToString());
|
|
return {};
|
|
}
|
|
}
|
|
|
|
std::string AlertModelImpl::GetState(const types::TextEventKey& key)
|
|
{
|
|
auto messageList = manager::TextEventManager::Instance()->message_list(key);
|
|
|
|
if (messageList.size() > 0)
|
|
{
|
|
auto& lastMessage = messageList.back();
|
|
size_t segmentCount = lastMessage->segment_count();
|
|
auto lastSegment = lastMessage->segment(segmentCount - 1);
|
|
return scwx::util::ToString(lastSegment->header_->ugc_.states());
|
|
}
|
|
else
|
|
{
|
|
logger_->warn("GetState(): No message associated with key: {}",
|
|
key.ToString());
|
|
return {};
|
|
}
|
|
}
|
|
|
|
std::chrono::system_clock::time_point
|
|
AlertModelImpl::GetStartTime(const types::TextEventKey& key)
|
|
{
|
|
auto messageList = manager::TextEventManager::Instance()->message_list(key);
|
|
|
|
if (messageList.size() > 0)
|
|
{
|
|
auto& firstMessage = messageList.front();
|
|
return firstMessage->segment_event_begin(0);
|
|
}
|
|
else
|
|
{
|
|
logger_->warn("GetStartTime(): No message associated with key: {}",
|
|
key.ToString());
|
|
return {};
|
|
}
|
|
}
|
|
|
|
std::string AlertModelImpl::GetStartTimeString(const types::TextEventKey& key)
|
|
{
|
|
return scwx::util::TimeString(GetStartTime(key));
|
|
}
|
|
|
|
std::chrono::system_clock::time_point
|
|
AlertModelImpl::GetEndTime(const types::TextEventKey& key)
|
|
{
|
|
auto messageList = manager::TextEventManager::Instance()->message_list(key);
|
|
|
|
if (messageList.size() > 0)
|
|
{
|
|
auto& lastMessage = messageList.back();
|
|
size_t segmentCount = lastMessage->segment_count();
|
|
auto lastSegment = lastMessage->segment(segmentCount - 1);
|
|
return lastSegment->header_->vtecString_[0].pVtec_.event_end();
|
|
}
|
|
else
|
|
{
|
|
logger_->warn("GetEndTime(): No message associated with key: {}",
|
|
key.ToString());
|
|
return {};
|
|
}
|
|
}
|
|
|
|
std::string AlertModelImpl::GetEndTimeString(const types::TextEventKey& key)
|
|
{
|
|
return scwx::util::TimeString(GetEndTime(key));
|
|
}
|
|
|
|
} // namespace model
|
|
} // namespace qt
|
|
} // namespace scwx
|