diff --git a/doc/img/AIS_plugin_map_3d.png b/doc/img/AIS_plugin_map_3d.png new file mode 100644 index 000000000..166e8143c Binary files /dev/null and b/doc/img/AIS_plugin_map_3d.png differ diff --git a/plugins/channelrx/demodais/CMakeLists.txt b/plugins/channelrx/demodais/CMakeLists.txt index 13ab930f2..d23a3823d 100644 --- a/plugins/channelrx/demodais/CMakeLists.txt +++ b/plugins/channelrx/demodais/CMakeLists.txt @@ -56,3 +56,8 @@ target_link_libraries(${TARGET_NAME} ) install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channelrx/demodais/readme.md b/plugins/channelrx/demodais/readme.md index 14abc0918..cb9104c90 100644 --- a/plugins/channelrx/demodais/readme.md +++ b/plugins/channelrx/demodais/readme.md @@ -6,7 +6,7 @@ This plugin can be used to demodulate AIS (Automatic Identification System) mess AIS is broadcast globally on 25kHz channels at 161.975MHz and 162.025MHz, with other frequencies being used regionally or for special purposes. This demodulator is single channel, so if you wish to decode multiple channels simulatenously, you will need to add one AIS demodulator per frequency. As most AIS messages are on 161.975MHz and 162.025MHz, you can set the center frequency as 162MHz, with a sample rate of 100k+Sa/s, with one AIS demod with an input offset -25kHz and another at +25kHz. -The AIS demodulators can send received messages to the AIS feature, which displays a table combining the latest data for vessels amalgamated from multiple demodulators and display their position on the Map Feature. +The AIS demodulators can send received messages to the [AIS feature](../../feature/ais/readme.md), which displays a table combining the latest data for vessels amalgamated from multiple demodulators and sends their positiosn to the [Map Feature](../../feature/map/readme.ais) for display in 2D or 3D. AIS uses GMSK/FM modulation at a baud rate of 9,600, with a modulation index of 0.5. The demodulator works at a sample rate of 57,600Sa/s. diff --git a/plugins/feature/ais/CMakeLists.txt b/plugins/feature/ais/CMakeLists.txt index 36f0415d2..ea46a9386 100644 --- a/plugins/feature/ais/CMakeLists.txt +++ b/plugins/feature/ais/CMakeLists.txt @@ -53,3 +53,8 @@ target_link_libraries(${TARGET_NAME} ) install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/feature/ais/aisgui.cpp b/plugins/feature/ais/aisgui.cpp index 9aae0e98f..b70216438 100644 --- a/plugins/feature/ais/aisgui.cpp +++ b/plugins/feature/ais/aisgui.cpp @@ -17,10 +17,12 @@ /////////////////////////////////////////////////////////////////////////////////// #include + #include #include #include #include +#include #include "feature/featureuiset.h" #include "feature/featurewebapiutils.h" @@ -34,6 +36,55 @@ #include "SWGMapItem.h" +// Models to use for ships when type is unknown +// Use as many as possibly, so it doesn't look too samey, but don't use +// the massive ships +QStringList AISGUI::m_shipModels = { + "ship_27m.glbe", "ship_65m.glbe", + "tug_20m.glbe", "tug_30m_1.glbe", "tug_30m_2.glbe", "tug_30m_3.glbe", + "cargo_75m.glbe", "tanker_50m.glbe", "dredger_53m.glbe", + "trawler_22m.glbe", + "speedboat_8m.glbe", "yacht_10m.glbe", "yacht_20m.glbe", "yacht_42m.glbe" +}; + +QStringList AISGUI::m_sailboatModels = { + "sailboat_8m.glbe", "sailboat_17m.glbe" +}; + +QHash AISGUI::m_labelOffset = { + {"helicopter.glb", 4.0f}, + {"antenna.glb", 4.5f}, + {"buoy.glb", 1.5f}, + {"ship_27m.glbe", 13.0f}, + {"dredger_53m.glbe", 19.0f}, + {"ship_65m.glbe", 26.0f}, + {"tug_20m.glbe", 10.0f}, + {"tug_30m_1.glbe", 17.0f}, + {"tug_30m_2.glbe", 17.0f}, + {"tug_30m_3.glbe", 17.0f}, + {"coastguard.glbe", 4.0}, + {"cargo_75m.glbe", 22.0f}, + {"cargo_190m.glbe", 42.0f}, + {"cargo_230m.glbe", 42.0f}, + {"tanker_50m.glbe", 12.0f}, + {"tanker_180m.glbe", 35.0f}, + {"tanker_245m_1.glbe", 30.0f}, + {"tanker_380m_1.glbe", 42.0f}, + {"passenger_100m.glbe", 34.0f}, + {"dredger_53m.glbe", 19.0f}, + {"trawler_22m.glbe", 15.0f}, + {"sailboat_8m.glbe", 11.0f}, + {"sailboat_17m.glbe", 24.0f}, + {"speedboat_8m.glbe", 3.0f}, + {"yacht_10m.glbe", 3.0f}, + {"yacht_20m.glbe", 7.5f}, + {"yacht_42m.glbe", 10.0f}, +}; + +QHash AISGUI::m_modelOffset = { + {"helicopter.glb", 4.0f}, +}; + AISGUI* AISGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) { AISGUI* gui = new AISGUI(pluginAPI, featureUISet, feature); @@ -92,7 +143,7 @@ bool AISGUI::handleMessage(const Message& message) // Decode the message AISMessage *ais = AISMessage::decode(report.getPacket()); // Update table - updateVessels(ais); + updateVessels(ais, report.getDateTime()); } return false; @@ -140,8 +191,9 @@ AISGUI::AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur 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); + // Timer to remove vessels we haven't heard from in a while + connect(&m_timer, SIGNAL(timeout()), this, SLOT(removeOldVessels())); + m_timer.start(60*1000); // Resize the table using dummy data resizeTable(); @@ -161,6 +213,9 @@ AISGUI::AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur // 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))); + // Context menu + ui->vessels->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->vessels, SIGNAL(customContextMenuRequested(QPoint)), SLOT(vessels_customContextMenuRequested(QPoint))); m_settings.setRollupState(&m_rollupState); @@ -170,6 +225,7 @@ AISGUI::AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur AISGUI::~AISGUI() { + qDeleteAll(m_vessels); delete ui; } @@ -243,8 +299,32 @@ void AISGUI::onMenuDialogCalled(const QPoint &p) resetContextMenuType(); } -void AISGUI::updateStatus() +void AISGUI::removeOldVessels() { + // Remove if we haven't received a message in 10 minutes + QDateTime currentDateTime = QDateTime::currentDateTime(); + for (int row = ui->vessels->rowCount() - 1; row >= 0; row--) + { + QDateTime lastDateTime = ui->vessels->item(row, VESSEL_COL_LAST_UPDATE)->data(Qt::DisplayRole).toDateTime(); + if (lastDateTime.isValid()) + { + qint64 diff = lastDateTime.secsTo(currentDateTime); + if (diff > 10*60) + { + QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); + // Remove from map + sendToMap(mmsi, "", + "", "", + "", 0.0f, 0.0f, + 0.0f, 0.0f, QDateTime(), + 0.0f); + // Remove from table + ui->vessels->removeRow(row); + // Remove from hash + m_vessels.remove(mmsi); + } + } + } } void AISGUI::applySettings(bool force) @@ -273,7 +353,11 @@ void AISGUI::resizeTable() 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_LENGTH, new QTableWidgetItem("400")); ui->vessels->setItem(row, VESSEL_COL_DESTINATION, new QTableWidgetItem("12345678901234567890")); + ui->vessels->setItem(row, VESSEL_COL_POSITION_UPDATE, new QTableWidgetItem("12/12/2022 12:00")); + ui->vessels->setItem(row, VESSEL_COL_LAST_UPDATE, new QTableWidgetItem("12/12/2022 12:00")); + ui->vessels->setItem(row, VESSEL_COL_MESSAGES, new QTableWidgetItem("1000")); ui->vessels->resizeColumnsToContents(); ui->vessels->removeRow(row); } @@ -324,7 +408,58 @@ QAction *AISGUI::createCheckableItem(QString &text, int idx, bool checked, const return action; } -void AISGUI::updateVessels(AISMessage *ais) +// Send to Map feature +void AISGUI::sendToMap(const QString &name, const QString &label, + const QString &image, const QString &text, + const QString &model, float modelOffset, float labelOffset, + float latitude, float longitude, QDateTime positionDateTime, + float heading + ) +{ + 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(name)); + swgMapItem->setLatitude(latitude); + swgMapItem->setLongitude(longitude); + swgMapItem->setAltitude(0); + swgMapItem->setAltitudeReference(1); // CLAMP_TO_GROUND + + if (positionDateTime.isValid()) { + swgMapItem->setPositionDateTime(new QString(positionDateTime.toString(Qt::ISODateWithMs))); + } + + swgMapItem->setImageRotation(heading); + swgMapItem->setText(new QString(text)); + + if (image.isEmpty()) { + swgMapItem->setImage(new QString("")); + } else { + swgMapItem->setImage(new QString(QString("qrc:///ais/map/%1").arg(image))); + } + swgMapItem->setModel(new QString(model)); + swgMapItem->setLabel(new QString(label)); + swgMapItem->setLabelAltitudeOffset(labelOffset); + swgMapItem->setFixedPosition(false); + swgMapItem->setOrientation(1); + swgMapItem->setHeading(heading); + swgMapItem->setPitch(0.0); + swgMapItem->setRoll(0.0); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ais, swgMapItem); + (*it)->push(msg); + } + } +} + +// Update table with received message +void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) { QTableWidgetItem *mmsiItem; QTableWidgetItem *typeItem; @@ -338,7 +473,15 @@ void AISGUI::updateVessels(AISMessage *ais) QTableWidgetItem *nameItem; QTableWidgetItem *callsignItem; QTableWidgetItem *shipTypeItem; + QTableWidgetItem *lengthItem; QTableWidgetItem *destinationItem; + QTableWidgetItem *positionUpdateItem; + QTableWidgetItem *lastUpdateItem; + QTableWidgetItem *messagesItem; + + QString previousType; + QString previousShipType; + Vessel *vessel; // See if vessel is already in table QString messageMMSI = QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')); @@ -361,7 +504,12 @@ void AISGUI::updateVessels(AISMessage *ais) 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); + lengthItem = ui->vessels->item(row, VESSEL_COL_LENGTH); destinationItem = ui->vessels->item(row, VESSEL_COL_DESTINATION); + positionUpdateItem = ui->vessels->item(row, VESSEL_COL_POSITION_UPDATE); + lastUpdateItem = ui->vessels->item(row, VESSEL_COL_LAST_UPDATE); + messagesItem = ui->vessels->item(row, VESSEL_COL_MESSAGES); + vessel = m_vessels.value(messageMMSI); found = true; break; } @@ -385,7 +533,11 @@ void AISGUI::updateVessels(AISMessage *ais) nameItem = new QTableWidgetItem(); callsignItem = new QTableWidgetItem(); shipTypeItem = new QTableWidgetItem(); + lengthItem = new QTableWidgetItem(); destinationItem = new QTableWidgetItem(); + positionUpdateItem = new QTableWidgetItem(); + lastUpdateItem = new QTableWidgetItem(); + messagesItem = 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); @@ -398,10 +550,24 @@ void AISGUI::updateVessels(AISMessage *ais) 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_LENGTH, lengthItem); ui->vessels->setItem(row, VESSEL_COL_DESTINATION, destinationItem); + ui->vessels->setItem(row, VESSEL_COL_POSITION_UPDATE, positionUpdateItem); + ui->vessels->setItem(row, VESSEL_COL_LAST_UPDATE, lastUpdateItem); + ui->vessels->setItem(row, VESSEL_COL_MESSAGES, messagesItem); + messagesItem->setData(Qt::DisplayRole, 0); + + vessel = new Vessel(); + m_vessels.insert(messageMMSI, vessel); } + previousType = typeItem->text(); + previousShipType = shipTypeItem->text(); + mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); + lastUpdateItem->setData(Qt::DisplayRole, dateTime); + messagesItem->setData(Qt::DisplayRole, messagesItem->data(Qt::DisplayRole).toInt() + 1); + 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) { @@ -429,6 +595,7 @@ void AISGUI::updateVessels(AISMessage *ais) nameItem->setText(vd->m_name); callsignItem->setText(vd->m_callsign); shipTypeItem->setText(AISMessage::typeToString(vd->m_type)); + lengthItem->setData(Qt::DisplayRole, vd->m_a + vd->m_b); destinationItem->setText(vd->m_destination); } } @@ -438,6 +605,7 @@ void AISGUI::updateVessels(AISMessage *ais) { latitudeItem->setData(Qt::DisplayRole, ais->getLatitude()); longitudeItem->setData(Qt::DisplayRole, ais->getLongitude()); + positionUpdateItem->setData(Qt::DisplayRole, dateTime); } if (ais->hasCourse()) { courseItem->setData(Qt::DisplayRole, ais->getCourse()); @@ -488,88 +656,211 @@ void AISGUI::updateVessels(AISMessage *ais) 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) + // Image and model to use on map + QString shipType = shipTypeItem->text(); + int length = lengthItem->data(Qt::DisplayRole).toInt(); + QString status = statusItem->text(); + + // Only update model if change in type - so we don't keeping picking new + // random models + if ((previousType != type) || (previousShipType != shipType)) { + getImageAndModel(type, shipType, length, status, vessel); + } + + float labelOffset = m_labelOffset.value(vessel->m_model); + float modelOffset = 0.0f; + if (m_modelOffset.contains(vessel->m_model)) { + modelOffset = m_modelOffset.value(vessel->m_model); + } + + // Text to display in info box + 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(); + float heading = 0.0f; + 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()) { - QList::iterator it = mapMessageQueues->begin(); + float course = courseV.toFloat(); + text.append(QString("Course: %1%2").arg(course).arg(QChar(0xb0))); + heading = course; + } + if (!speedV.isNull()) { + text.append(QString("Speed: %1 knts").arg(speedV.toFloat())); + } + if (!headingV.isNull()) + { + heading = headingV.toFloat(); + text.append(QString("Heading: %1%2").arg(heading).arg(QChar(0xb0))); + } + if (!shipType.isEmpty()) { + text.append(QString("Ship type: %1").arg(shipType)); + } + if (!status.isEmpty()) { + text.append(QString("Status: %1").arg(status)); + } - for (; it != mapMessageQueues->end(); ++it) + // Send to map feature + sendToMap(mmsiItem->text(), callsign, + vessel->m_image, text.join("
"), + vessel->m_model, modelOffset, labelOffset, + latitudeV.toFloat(), longitudeV.toFloat(), positionUpdateItem->data(Qt::DisplayRole).toDateTime(), + heading); + } +} + +void AISGUI::getImageAndModel(const QString &type, const QString &shipType, int length, const QString &status, Vessel *vessel) +{ + if (type == "Aircraft") + { + // I presume search and rescue aircraft are more likely to be helicopters + vessel->m_image = "helicopter.png"; + vessel->m_model = "helicopter.glb"; + } + else if (type == "Base station") + { + vessel->m_image = "anchor.png"; + vessel->m_model = "antenna.glb"; + } + else if (type == "Aid-to-nav") + { + vessel->m_image = "bouy.png"; + vessel->m_model = "buoy.glb"; + } + else + { + vessel->m_image = "ship.png"; + if (status == "Under way sailing") { + vessel->m_model = m_sailboatModels[m_random.bounded(m_sailboatModels.size())]; + } else { + vessel->m_model = m_shipModels[m_random.bounded(m_shipModels.size())]; + } + + if (!shipType.isEmpty()) + { + if (shipType == "Ship") { - 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()) + if (length < 40) { - float course = courseV.toFloat(); - text.append(QString("Course: %1%2").arg(course).arg(QChar(0xb0))); - swgMapItem->setImageRotation(course); + vessel->m_model = "ship_27m.glbe"; } - if (!speedV.isNull()) { - text.append(QString("Speed: %1 knts").arg(speedV.toFloat())); - } - if (!headingV.isNull()) + else if (length < 60) { - float heading = headingV.toFloat(); - text.append(QString("Heading: %1%2").arg(heading).arg(QChar(0xb0))); - swgMapItem->setImageRotation(heading); // heading takes precedence over course + vessel->m_model = "dredger_53m.glbe"; } - if (!shipType.isEmpty()) { - text.append(QString("Ship type: %1").arg(shipType)); + else + { + vessel->m_model = "ship_65m.glbe"; } - if (!status.isEmpty()) { - text.append(QString("Status: %1").arg(status)); + } + else if ((shipType == "Tug") || (shipType == "Port tender") || (shipType == "Pilot vessel")) + { + vessel->m_image = "tug.png"; + if (length < 25) + { + vessel->m_model = "tug_20m.glbe"; + } + else + { + int rand = m_random.bounded(1, 3); + vessel->m_model = QString("tug_30m_%1.glbe").arg(rand); + } + } + else if ((shipType == "Law enforcement vessel") || (shipType == "Search and rescue vessel")) + { + vessel->m_model = "coastguard.glbe"; + } + else if (shipType == "Cargo") + { + vessel->m_image = "cargo.png"; + if (length < 120) + { + vessel->m_model = "cargo_75m.glbe"; + } + else if (length < 200) + { + vessel->m_model = "cargo_190m.glbe"; + } + else + { + vessel->m_model = "cargo_230m.glbe"; + } + } + else if (shipType == "Tanker") + { + vessel->m_image = "tanker.png"; + if (length < 120) + { + vessel->m_model = "tanker_50m.glbe"; + } + else if (length < 210) + { + vessel->m_model = "tanker_180m.glbe"; + } + else if (length < 300) + { + int rand = m_random.bounded(1, 4); + vessel->m_model = QString("tanker_245m_%1.glbe").arg(rand); + } + else + { + int rand = m_random.bounded(1, 3); + vessel->m_model = QString("tanker_380m_%1.glbe").arg(rand); + } + } + else if (shipType == "Passenger") + { + vessel->m_model = "passenger_100m.glbe"; + } + else if (shipType == "Vessel - Dredging or underwater operations") + { + vessel->m_model = "dredger_53m.glbe"; + } + else if (shipType == "Vessel - Fishing") + { + vessel->m_model = "trawler_22m.glbe"; + } + else if (shipType == "Vessel - Sailing") + { + if (length < 13) + { + vessel->m_model = "sailboat_8m.glbe"; + } + else + { + vessel->m_model = "sailboat_17m.glbe"; + } + } + else if (shipType.contains("Pleasure craft")) + { + if (length < 9) + { + vessel->m_model = "speedboat_8m.glbe"; + } + else if (length < 18) + { + vessel->m_model = "yacht_10m.glbe"; + } + else if (length < 32) + { + vessel->m_model = "yacht_20m.glbe"; + } + else + { + vessel->m_model = "yacht_42m.glbe"; } - swgMapItem->setText(new QString(text.join("\n"))); - - MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ais, swgMapItem); - (*it)->push(msg); } } } @@ -584,7 +875,7 @@ void AISGUI::on_vessels_cellDoubleClicked(int row, int column) // 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)) + else if ((column == VESSEL_COL_LATITUDE) || (column == VESSEL_COL_LONGITUDE) || (column == VESSEL_COL_SHIP_TYPE)) { // Get MMSI of vessel in row double clicked QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); @@ -619,3 +910,93 @@ void AISGUI::on_vessels_cellDoubleClicked(int row, int column) } } } + +// Table cells context menu +void AISGUI::vessels_customContextMenuRequested(QPoint pos) +{ + QTableWidgetItem *item = ui->vessels->itemAt(pos); + if (item) + { + int row = item->row(); + QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); + QString imo = ui->vessels->item(row, VESSEL_COL_IMO)->text(); + QString name = ui->vessels->item(row, VESSEL_COL_NAME)->text(); + QVariant latitudeV = ui->vessels->item(row, VESSEL_COL_LATITUDE)->data(Qt::DisplayRole); + QVariant longitudeV = ui->vessels->item(row, VESSEL_COL_LONGITUDE)->data(Qt::DisplayRole); + QString destination = ui->vessels->item(row, VESSEL_COL_DESTINATION)->text(); + + QMenu* tableContextMenu = new QMenu(ui->vessels); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + + // Copy current cell + + QAction* copyAction = new QAction("Copy", tableContextMenu); + const QString text = item->text(); + connect(copyAction, &QAction::triggered, this, [text]()->void { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); + }); + tableContextMenu->addAction(copyAction); + tableContextMenu->addSeparator(); + + // View vessel on various websites + + QAction* mmsiAISHubAction = new QAction(QString("View MMSI %1 on aishub.net...").arg(mmsi), tableContextMenu); + connect(mmsiAISHubAction, &QAction::triggered, this, [mmsi]()->void { + QDesktopServices::openUrl(QUrl(QString("https://www.aishub.net/vessels?Ship%5Bmmsi%5D=%1&mmsi=%1").arg(mmsi))); + }); + tableContextMenu->addAction(mmsiAISHubAction); + + QAction* mmsiAction = new QAction(QString("View MMSI %1 on vesselfinder.com...").arg(mmsi), tableContextMenu); + connect(mmsiAction, &QAction::triggered, this, [mmsi]()->void { + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(mmsi))); + }); + tableContextMenu->addAction(mmsiAction); + + if (!imo.isEmpty()) + { + QAction* imoAction = new QAction(QString("View IMO %1 on vesselfinder.net...").arg(imo), tableContextMenu); + connect(imoAction, &QAction::triggered, this, [imo]()->void { + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(imo))); + }); + tableContextMenu->addAction(imoAction); + } + + if (!name.isEmpty()) + { + QAction* nameAction = new QAction(QString("View %1 on vesselfinder.net...").arg(name), tableContextMenu); + connect(nameAction, &QAction::triggered, this, [name]()->void { + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(name))); + }); + tableContextMenu->addAction(nameAction); + } + + // Find on Map + if (!latitudeV.isNull()) + { + float latitude = latitudeV.toFloat(); + float longitude = longitudeV.toFloat(); + + tableContextMenu->addSeparator(); + + QAction* findMapFeatureAction = new QAction(QString("Find MMSI %1 on map").arg(mmsi), tableContextMenu); + connect(findMapFeatureAction, &QAction::triggered, this, [mmsi]()->void { + FeatureWebAPIUtils::mapFind(mmsi); + }); + tableContextMenu->addAction(findMapFeatureAction); + } + + if (!destination.isEmpty()) + { + tableContextMenu->addSeparator(); + + QAction* findDestinationFeatureAction = new QAction(QString("Find %1 on map").arg(destination), tableContextMenu); + connect(findDestinationFeatureAction, &QAction::triggered, this, [destination]()->void { + FeatureWebAPIUtils::mapFind(destination); + }); + tableContextMenu->addAction(findDestinationFeatureAction); + } + + tableContextMenu->popup(ui->vessels->viewport()->mapToGlobal(pos)); + } +} diff --git a/plugins/feature/ais/aisgui.h b/plugins/feature/ais/aisgui.h index 184e2d5cb..797700e7f 100644 --- a/plugins/feature/ais/aisgui.h +++ b/plugins/feature/ais/aisgui.h @@ -21,6 +21,9 @@ #include #include +#include +#include +#include #include "feature/featuregui.h" #include "util/messagequeue.h" @@ -40,6 +43,13 @@ namespace Ui { class AISGUI : public FeatureGUI { Q_OBJECT + + // Holds information not in the table + struct Vessel { + QString m_image; + QString m_model; + }; + public: static AISGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); virtual void destroy(); @@ -59,9 +69,16 @@ private: AIS* m_ais; MessageQueue m_inputMessageQueue; - QTimer m_statusTimer; + QTimer m_timer; int m_lastFeatureState; + QRandomGenerator m_random; + QHash m_vessels; // Hash of mmsi to vessels + static QStringList m_shipModels; + static QStringList m_sailboatModels; + static QHash m_labelOffset; + static QHash m_modelOffset; + QMenu *vesselsMenu; // Column select context menu explicit AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); @@ -75,7 +92,14 @@ private: void leaveEvent(QEvent*); void enterEvent(QEvent*); - void updateVessels(AISMessage *ais); + void AISGUI::sendToMap(const QString &name, const QString &label, + const QString &image, const QString &text, + const QString &model, float modelOffset, float labelOffset, + float latitude, float longitude, QDateTime positionDateTime, + float heading + ); + void updateVessels(AISMessage *ais, QDateTime dateTime); + void getImageAndModel(const QString &type, const QString &shipType, int length, const QString &status, Vessel *vessel); void resizeTable(); QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot); @@ -92,19 +116,24 @@ private: VESSEL_COL_NAME, VESSEL_COL_CALLSIGN, VESSEL_COL_SHIP_TYPE, - VESSEL_COL_DESTINATION + VESSEL_COL_LENGTH, + VESSEL_COL_DESTINATION, + VESSEL_COL_POSITION_UPDATE, + VESSEL_COL_LAST_UPDATE, + VESSEL_COL_MESSAGES }; private slots: void onMenuDialogCalled(const QPoint &p); void onWidgetRolled(QWidget* widget, bool rollDown); void handleInputMessages(); - void updateStatus(); void on_vessels_cellDoubleClicked(int row, int column); + void vessels_customContextMenuRequested(QPoint pos); void vessels_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); void vessels_sectionResized(int logicalIndex, int oldSize, int newSize); void vesselsColumnSelectMenu(QPoint pos); void vesselsColumnSelectMenuChecked(bool checked = false); + void removeOldVessels(); }; #endif // INCLUDE_FEATURE_AISGUI_H_ diff --git a/plugins/feature/ais/aisgui.ui b/plugins/feature/ais/aisgui.ui index b1a5bc871..5c3b9fb9f 100644 --- a/plugins/feature/ais/aisgui.ui +++ b/plugins/feature/ais/aisgui.ui @@ -30,7 +30,6 @@ - Liberation Sans 9 @@ -166,11 +165,40 @@ Ship Type + + + Length + + Destination + + + Position Updated + + + Time last position was received + + + + + Updated + + + Time last message was received + + + + + Messages + + + Number of messages received + + diff --git a/plugins/feature/ais/aissettings.h b/plugins/feature/ais/aissettings.h index 49a383490..d29fe3f84 100644 --- a/plugins/feature/ais/aissettings.h +++ b/plugins/feature/ais/aissettings.h @@ -27,7 +27,7 @@ class Serializable; // Number of columns in the tables -#define AIS_VESSEL_COLUMNS 13 +#define AIS_VESSEL_COLUMNS 16 struct AISSettings { diff --git a/plugins/feature/ais/readme.md b/plugins/feature/ais/readme.md index 7d2ae916e..aec25873f 100644 --- a/plugins/feature/ais/readme.md +++ b/plugins/feature/ais/readme.md @@ -2,9 +2,11 @@

Introduction

-The AIS feature displays a table containing the most recent information about vessels, base-stations and aids-to-navigation, based on messages received via AIS Demodulators. +The AIS feature displays a table containing the most recent information about vessels, base-stations and aids-to-navigation, +based on messages received via [AIS Demodulators](../../channelrx/demodais/readme.md). +Typically the AIS feature would be used with two AIS Demodulators: one at 161.975MHz and 162.025MHz. -The AIS feature can draw corresponding objects on the Map. +The AIS feature can draw corresponding objects on the Map in 2D and 3D.

Interface

@@ -26,16 +28,26 @@ The vessels table displays the current status for each vessel, base station or a * Name - Name of the vessel. Double clicking on this column will search for the name on https://www.vesselfinder.com/ * Callsign - Callsign of the vessel. * Ship Type - Type of ship (E.g. Passenger ship, Cargo ship, Tanker) and activity (Fishing, Towing, Sailing). +* Length - The length of the vessel. * Destination - Destination the vessel is travelling to. Double clicking on this column will search for this location on the map on this object. +* Position Updated - Gives the date and time the last position was received. +* Updated - Gives the date and time the last message was received. +* Messages - Displays the number of messages received. Right clicking on the table header allows you to select which columns to show. The columns can be reorderd by left clicking and dragging the column header. +Right clicking on a table cell allows you to copy the cell contents, view the vessel on a variety of websites or find the vessel on the map. + +Vessels are removed from the table if a message is not received for 10 minutes. +

Map

-The AIS feature can plot ships, base stations and aids-to-navigation on the Map. To use, simply open a Map feature and the AIS plugin will display objects based upon the messages it receives from that point. +The AIS feature can plot ships, base stations and aids-to-navigation on the [Map](../../feature/map/readme.md). To use, simply open a Map feature and the AIS plugin will display objects based upon the messages it receives from that point. Selecting an AIS item on the map will display a text bubble containing information from the above table. To centre the map on an item in the table, double click in the Lat or Lon columns. -![AIS map](../../../doc/img/AIS_plugin_map.png) +![AIS 2D map](../../../doc/img/AIS_plugin_map.png) + +![AIS 3D map](../../../doc/img/AIS_plugin_map_3d.png)

Attribution