/////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2020 Jon Beniston, M7RCE // // Copyright (C) 2020 Edouard Griffiths, F4EXB // // // // This program is free software; you can redistribute it and/or modify // // it under the terms of the GNU General Public License as published by // // the Free Software Foundation as version 3 of the License, or // // (at your option) any later version. // // // // This program is distributed in the hope that it will be useful, // // but WITHOUT ANY WARRANTY; without even the implied warranty of // // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // // GNU General Public License V3 for more details. // // // // You should have received a copy of the GNU General Public License // // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include #include "feature/featureuiset.h" #include "feature/featurewebapiutils.h" #include "gui/basicfeaturesettingsdialog.h" #include "mainwindow.h" #include "device/deviceuiset.h" #include "ui_aisgui.h" #include "ais.h" #include "aisgui.h" #include "SWGMapItem.h" AISGUI* AISGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) { AISGUI* gui = new AISGUI(pluginAPI, featureUISet, feature); return gui; } void AISGUI::destroy() { delete this; } void AISGUI::resetToDefaults() { m_settings.resetToDefaults(); displaySettings(); applySettings(true); } QByteArray AISGUI::serialize() const { return m_settings.serialize(); } bool AISGUI::deserialize(const QByteArray& data) { if (m_settings.deserialize(data)) { displaySettings(); applySettings(true); return true; } else { resetToDefaults(); return false; } } bool AISGUI::handleMessage(const Message& message) { if (AIS::MsgConfigureAIS::match(message)) { qDebug("AISGUI::handleMessage: AIS::MsgConfigureAIS"); const AIS::MsgConfigureAIS& cfg = (AIS::MsgConfigureAIS&) message; m_settings = cfg.getSettings(); blockApplySettings(true); displaySettings(); blockApplySettings(false); return true; } else if (MainCore::MsgPacket::match(message)) { MainCore::MsgPacket& report = (MainCore::MsgPacket&) message; // Decode the message AISMessage *ais = AISMessage::decode(report.getPacket()); // Update table updateVessels(ais); } return false; } void AISGUI::handleInputMessages() { Message* message; while ((message = getInputMessageQueue()->pop())) { if (handleMessage(*message)) { delete message; } } } void AISGUI::onWidgetRolled(QWidget* widget, bool rollDown) { (void) widget; (void) rollDown; m_settings.m_rollupState = saveState(); applySettings(); } AISGUI::AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : FeatureGUI(parent), ui(new Ui::AISGUI), m_pluginAPI(pluginAPI), m_featureUISet(featureUISet), m_doApplySettings(true), m_lastFeatureState(0) { ui->setupUi(this); m_helpURL = "plugins/feature/ais/readme.md"; setAttribute(Qt::WA_DeleteOnClose, true); setChannelWidget(false); connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); m_ais = reinterpret_cast(feature); m_ais->setMessageQueueToGUI(&m_inputMessageQueue); m_featureUISet->addRollupWidget(this); connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); m_statusTimer.start(1000); // Resize the table using dummy data resizeTable(); // Allow user to reorder columns ui->vessels->horizontalHeader()->setSectionsMovable(true); // Allow user to sort table by clicking on headers ui->vessels->setSortingEnabled(true); // Add context menu to allow hiding/showing of columns vesselsMenu = new QMenu(ui->vessels); for (int i = 0; i < ui->vessels->horizontalHeader()->count(); i++) { QString text = ui->vessels->horizontalHeaderItem(i)->text(); vesselsMenu->addAction(createCheckableItem(text, i, true, SLOT(vesselsColumnSelectMenuChecked()))); } ui->vessels->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->vessels->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(vesselsColumnSelectMenu(QPoint))); // Get signals when columns change connect(ui->vessels->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(vessels_sectionMoved(int, int, int))); connect(ui->vessels->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(vessels_sectionResized(int, int, int))); displaySettings(); applySettings(true); } AISGUI::~AISGUI() { delete ui; } void AISGUI::blockApplySettings(bool block) { m_doApplySettings = !block; } void AISGUI::displaySettings() { setTitleColor(m_settings.m_rgbColor); setWindowTitle(m_settings.m_title); blockApplySettings(true); // Order and size columns QHeaderView *header = ui->vessels->horizontalHeader(); for (int i = 0; i < AIS_VESSEL_COLUMNS; i++) { bool hidden = m_settings.m_vesselColumnSizes[i] == 0; header->setSectionHidden(i, hidden); vesselsMenu->actions().at(i)->setChecked(!hidden); if (m_settings.m_vesselColumnSizes[i] > 0) { ui->vessels->setColumnWidth(i, m_settings.m_vesselColumnSizes[i]); } header->moveSection(header->visualIndex(i), m_settings.m_vesselColumnIndexes[i]); } restoreState(m_settings.m_rollupState); blockApplySettings(false); arrangeRollups(); } void AISGUI::leaveEvent(QEvent*) { } void AISGUI::enterEvent(QEvent*) { } void AISGUI::onMenuDialogCalled(const QPoint &p) { if (m_contextMenuType == ContextMenuChannelSettings) { BasicFeatureSettingsDialog dialog(this); dialog.setTitle(m_settings.m_title); dialog.setColor(m_settings.m_rgbColor); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); dialog.move(p); dialog.exec(); m_settings.m_rgbColor = dialog.getColor().rgb(); m_settings.m_title = dialog.getTitle(); m_settings.m_useReverseAPI = dialog.useReverseAPI(); m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); setWindowTitle(m_settings.m_title); setTitleColor(m_settings.m_rgbColor); applySettings(); } resetContextMenuType(); } void AISGUI::updateStatus() { } void AISGUI::applySettings(bool force) { if (m_doApplySettings) { AIS::MsgConfigureAIS* message = AIS::MsgConfigureAIS::create(m_settings, force); m_ais->getInputMessageQueue()->push(message); } } void AISGUI::resizeTable() { // Fill table with a row of dummy data that will size the columns nicely int row = ui->vessels->rowCount(); ui->vessels->setRowCount(row + 1); ui->vessels->setItem(row, VESSEL_COL_MMSI, new QTableWidgetItem("123456789")); ui->vessels->setItem(row, VESSEL_COL_TYPE, new QTableWidgetItem("Base station")); ui->vessels->setItem(row, VESSEL_COL_LATITUDE, new QTableWidgetItem("90.000000-")); ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, new QTableWidgetItem("180.00000-")); ui->vessels->setItem(row, VESSEL_COL_COURSE, new QTableWidgetItem("360.0")); ui->vessels->setItem(row, VESSEL_COL_SPEED, new QTableWidgetItem("120")); ui->vessels->setItem(row, VESSEL_COL_HEADING, new QTableWidgetItem("360")); ui->vessels->setItem(row, VESSEL_COL_STATUS, new QTableWidgetItem("Under way using engine")); ui->vessels->setItem(row, VESSEL_COL_IMO, new QTableWidgetItem("123456789")); ui->vessels->setItem(row, VESSEL_COL_NAME, new QTableWidgetItem("12345678901234567890")); ui->vessels->setItem(row, VESSEL_COL_CALLSIGN, new QTableWidgetItem("1234567")); ui->vessels->setItem(row, VESSEL_COL_SHIP_TYPE, new QTableWidgetItem("Passenger")); ui->vessels->setItem(row, VESSEL_COL_DESTINATION, new QTableWidgetItem("12345678901234567890")); ui->vessels->resizeColumnsToContents(); ui->vessels->removeRow(row); } // Columns in table reordered void AISGUI::vessels_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) { (void) oldVisualIndex; m_settings.m_vesselColumnIndexes[logicalIndex] = newVisualIndex; } // Column in table resized (when hidden size is 0) void AISGUI::vessels_sectionResized(int logicalIndex, int oldSize, int newSize) { (void) oldSize; m_settings.m_vesselColumnSizes[logicalIndex] = newSize; } // Right click in table header - show column select menu void AISGUI::vesselsColumnSelectMenu(QPoint pos) { vesselsMenu->popup(ui->vessels->horizontalHeader()->viewport()->mapToGlobal(pos)); } // Hide/show column when menu selected void AISGUI::vesselsColumnSelectMenuChecked(bool checked) { (void) checked; QAction* action = qobject_cast(sender()); if (action != nullptr) { int idx = action->data().toInt(nullptr); ui->vessels->setColumnHidden(idx, !action->isChecked()); } } // Create column select menu item QAction *AISGUI::createCheckableItem(QString &text, int idx, bool checked, const char *slot) { QAction *action = new QAction(text, this); action->setCheckable(true); action->setChecked(checked); action->setData(QVariant(idx)); connect(action, SIGNAL(triggered()), this, slot); return action; } void AISGUI::updateVessels(AISMessage *ais) { QTableWidgetItem *mmsiItem; QTableWidgetItem *typeItem; QTableWidgetItem *latitudeItem; QTableWidgetItem *longitudeItem; QTableWidgetItem *courseItem; QTableWidgetItem *speedItem; QTableWidgetItem *headingItem; QTableWidgetItem *statusItem; QTableWidgetItem *imoItem; QTableWidgetItem *nameItem; QTableWidgetItem *callsignItem; QTableWidgetItem *shipTypeItem; QTableWidgetItem *destinationItem; // See if vessel is already in table QString messageMMSI = QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')); bool found = false; for (int row = 0; row < ui->vessels->rowCount(); row++) { QString itemMMSI = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); if (messageMMSI == itemMMSI) { // Update existing item mmsiItem = ui->vessels->item(row, VESSEL_COL_MMSI); typeItem = ui->vessels->item(row, VESSEL_COL_TYPE); latitudeItem = ui->vessels->item(row, VESSEL_COL_LATITUDE); longitudeItem = ui->vessels->item(row, VESSEL_COL_LONGITUDE); courseItem = ui->vessels->item(row, VESSEL_COL_COURSE); speedItem = ui->vessels->item(row, VESSEL_COL_SPEED); headingItem = ui->vessels->item(row, VESSEL_COL_HEADING); statusItem = ui->vessels->item(row, VESSEL_COL_STATUS); imoItem = ui->vessels->item(row, VESSEL_COL_IMO); nameItem = ui->vessels->item(row, VESSEL_COL_NAME); callsignItem = ui->vessels->item(row, VESSEL_COL_CALLSIGN); shipTypeItem = ui->vessels->item(row, VESSEL_COL_SHIP_TYPE); destinationItem = ui->vessels->item(row, VESSEL_COL_DESTINATION); found = true; break; } } if (!found) { // Add new vessel ui->vessels->setSortingEnabled(false); int row = ui->vessels->rowCount(); ui->vessels->setRowCount(row + 1); mmsiItem = new QTableWidgetItem(); typeItem = new QTableWidgetItem(); latitudeItem = new QTableWidgetItem(); longitudeItem = new QTableWidgetItem(); courseItem = new QTableWidgetItem(); speedItem = new QTableWidgetItem(); headingItem = new QTableWidgetItem(); statusItem = new QTableWidgetItem(); imoItem = new QTableWidgetItem(); nameItem = new QTableWidgetItem(); callsignItem = new QTableWidgetItem(); shipTypeItem = new QTableWidgetItem(); destinationItem = new QTableWidgetItem(); ui->vessels->setItem(row, VESSEL_COL_MMSI, mmsiItem); ui->vessels->setItem(row, VESSEL_COL_TYPE, typeItem); ui->vessels->setItem(row, VESSEL_COL_LATITUDE, latitudeItem); ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, longitudeItem); ui->vessels->setItem(row, VESSEL_COL_COURSE, courseItem); ui->vessels->setItem(row, VESSEL_COL_SPEED, speedItem); ui->vessels->setItem(row, VESSEL_COL_HEADING, headingItem); ui->vessels->setItem(row, VESSEL_COL_STATUS, statusItem); ui->vessels->setItem(row, VESSEL_COL_IMO, imoItem); ui->vessels->setItem(row, VESSEL_COL_NAME, nameItem); ui->vessels->setItem(row, VESSEL_COL_CALLSIGN, callsignItem); ui->vessels->setItem(row, VESSEL_COL_SHIP_TYPE, shipTypeItem); ui->vessels->setItem(row, VESSEL_COL_DESTINATION, destinationItem); } mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); if ((ais->m_id <= 3) || (ais->m_id == 5) || (ais->m_id == 18) || (ais->m_id == 19)) { typeItem->setText("Vessel"); } else if (ais->m_id == 4) { typeItem->setText("Base station"); } else if (ais->m_id == 9) { typeItem->setText("Aircraft"); } else if (ais->m_id == 21) { typeItem->setText("Aid-to-nav"); } if (ais->m_id == 21) { AISAidsToNavigationReport *aids = dynamic_cast(ais); if (aids) { nameItem->setText(aids->m_name); } } if (ais->m_id == 5) { AISShipStaticAndVoyageData *vd = dynamic_cast(ais); if (vd) { if (vd->m_imo != 0) { imoItem->setData(Qt::DisplayRole, vd->m_imo); } nameItem->setText(vd->m_name); callsignItem->setText(vd->m_callsign); shipTypeItem->setText(AISMessage::typeToString(vd->m_type)); destinationItem->setText(vd->m_destination); } } else { if (ais->hasPosition()) { latitudeItem->setData(Qt::DisplayRole, ais->getLatitude()); longitudeItem->setData(Qt::DisplayRole, ais->getLongitude()); } if (ais->hasCourse()) { courseItem->setData(Qt::DisplayRole, ais->getCourse()); } if (ais->hasSpeed()) { speedItem->setData(Qt::DisplayRole, ais->getSpeed()); } if (ais->hasHeading()) { headingItem->setData(Qt::DisplayRole, ais->getHeading()); } AISPositionReport *pr = dynamic_cast(ais); if (pr) { statusItem->setText(AISPositionReport::getStatusString(pr->m_status)); } AISLongRangePositionReport *lrpr = dynamic_cast(ais); if (lrpr) { statusItem->setText(AISPositionReport::getStatusString(lrpr->m_status)); } } if (ais->m_id == 19) { AISExtendedClassBPositionReport *ext = dynamic_cast(ais); if (ext) { shipTypeItem->setText(AISMessage::typeToString(ext->m_type)); } } if (ais->m_id == 24) { AISStaticDataReport *dr = dynamic_cast(ais); if (dr) { if (dr->m_partNumber == 0) { nameItem->setText(dr->m_name); } else if (dr->m_partNumber == 1) { callsignItem->setText(dr->m_callsign); shipTypeItem->setText(AISMessage::typeToString(dr->m_type)); } } } ui->vessels->setSortingEnabled(true); QVariant latitudeV = latitudeItem->data(Qt::DisplayRole); QVariant longitudeV = longitudeItem->data(Qt::DisplayRole); QString type = typeItem->text(); if (!latitudeV.isNull() && !longitudeV.isNull() && !type.isEmpty()) { // Send to Map feature MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); QList *mapMessageQueues = messagePipes.getMessageQueues(m_ais, "mapitems"); if (mapMessageQueues) { QList::iterator it = mapMessageQueues->begin(); for (; it != mapMessageQueues->end(); ++it) { SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); swgMapItem->setName(new QString(QString("%1").arg(mmsiItem->text()))); swgMapItem->setLatitude(latitudeV.toFloat()); swgMapItem->setLongitude(longitudeV.toFloat()); swgMapItem->setAltitude(0); QString image; if (type == "Aircraft") { // I presume search and rescue aircraft are more likely to be helicopters image = "helicopter.png"; } else if (type == "Base station") { image = "anchor.png"; } else if (type == "Aid-to-nav") { image = "bouy.png"; } else { image = "ship.png"; QString shipType = shipTypeItem->text(); if (!shipType.isEmpty()) { if (shipType == "Tug") { image = "tug.png"; } else if (shipType == "Cargo") { image = "cargo.png"; } else if (shipType == "Tanker") { image = "tanker.png"; } } } swgMapItem->setImage(new QString(QString("qrc:///ais/map/%1").arg(image))); swgMapItem->setImageMinZoom(11); QStringList text; QVariant courseV = courseItem->data(Qt::DisplayRole); QVariant speedV = speedItem->data(Qt::DisplayRole); QVariant headingV = headingItem->data(Qt::DisplayRole); QString name = nameItem->text(); QString callsign = callsignItem->text(); QString destination = destinationItem->text(); QString shipType = shipTypeItem->text(); QString status = statusItem->text(); if (!name.isEmpty()) { text.append(QString("Name: %1").arg(name)); } if (!callsign.isEmpty()) { text.append(QString("Callsign: %1").arg(callsign)); } if (!destination.isEmpty()) { text.append(QString("Destination: %1").arg(destination)); } if (!courseV.isNull()) { float course = courseV.toFloat(); text.append(QString("Course: %1%2").arg(course).arg(QChar(0xb0))); swgMapItem->setImageRotation(course); } if (!speedV.isNull()) { text.append(QString("Speed: %1 knts").arg(speedV.toFloat())); } if (!headingV.isNull()) { float heading = headingV.toFloat(); text.append(QString("Heading: %1%2").arg(heading).arg(QChar(0xb0))); swgMapItem->setImageRotation(heading); // heading takes precedence over course } if (!shipType.isEmpty()) { text.append(QString("Ship type: %1").arg(shipType)); } if (!status.isEmpty()) { text.append(QString("Status: %1").arg(status)); } swgMapItem->setText(new QString(text.join("\n"))); MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ais, swgMapItem); (*it)->push(msg); } } } } void AISGUI::on_vessels_cellDoubleClicked(int row, int column) { if (column == VESSEL_COL_MMSI) { // Get MMSI of vessel in row double clicked QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); // Search for MMSI on www.vesselfinder.com QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(mmsi))); } else if ((column == VESSEL_COL_LATITUDE) || (column == VESSEL_COL_LONGITUDE)) { // Get MMSI of vessel in row double clicked QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); // Find MMSI on Map FeatureWebAPIUtils::mapFind(mmsi); } else if (column == VESSEL_COL_IMO) { QString imo = ui->vessels->item(row, VESSEL_COL_IMO)->text(); if (!imo.isEmpty() && (imo != "0")) { // Search for IMO on www.vesselfinder.com QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(imo))); } } else if (column == VESSEL_COL_NAME) { QString name = ui->vessels->item(row, VESSEL_COL_NAME)->text(); if (!name.isEmpty()) { // Search for name on www.vesselfinder.com QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(name))); } } else if (column == VESSEL_COL_DESTINATION) { QString destination = ui->vessels->item(row, VESSEL_COL_DESTINATION)->text(); if (!destination.isEmpty()) { // Find destination on Map FeatureWebAPIUtils::mapFind(destination); } } }