mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-12-23 01:55:48 -05:00
AIS updates
Add support for 3D models. Remove vessels from table if not heard from in last 10 minutes. Add columns in table for vessel length, time last position & message were received and number of messages received. Add context menu.
This commit is contained in:
parent
9cc993ef8c
commit
635dbe4571
BIN
doc/img/AIS_plugin_map_3d.png
Normal file
BIN
doc/img/AIS_plugin_map_3d.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 443 KiB |
@ -56,3 +56,8 @@ target_link_libraries(${TARGET_NAME}
|
||||
)
|
||||
|
||||
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
|
||||
|
||||
# Install debug symbols
|
||||
if (WIN32)
|
||||
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
|
||||
endif()
|
||||
|
@ -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.
|
||||
|
||||
|
@ -53,3 +53,8 @@ target_link_libraries(${TARGET_NAME}
|
||||
)
|
||||
|
||||
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})
|
||||
|
||||
# Install debug symbols
|
||||
if (WIN32)
|
||||
install(FILES $<TARGET_PDB_FILE:${TARGET_NAME}> CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} )
|
||||
endif()
|
||||
|
@ -17,10 +17,12 @@
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QLineEdit>
|
||||
#include <QDesktopServices>
|
||||
#include <QAction>
|
||||
#include <QClipboard>
|
||||
|
||||
#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<QString, float> 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<QString, float> 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<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_ais, "mapitems");
|
||||
if (mapMessageQueues)
|
||||
{
|
||||
QList<MessageQueue*>::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,45 +656,24 @@ void AISGUI::updateVessels(AISMessage *ais)
|
||||
|
||||
if (!latitudeV.isNull() && !longitudeV.isNull() && !type.isEmpty())
|
||||
{
|
||||
// Send to Map feature
|
||||
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
|
||||
QList<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_ais, "mapitems");
|
||||
if (mapMessageQueues)
|
||||
{
|
||||
QList<MessageQueue*>::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";
|
||||
// Image and model to use on map
|
||||
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)));
|
||||
int length = lengthItem->data(Qt::DisplayRole).toInt();
|
||||
QString status = statusItem->text();
|
||||
|
||||
swgMapItem->setImageMinZoom(11);
|
||||
// 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);
|
||||
@ -534,8 +681,7 @@ void AISGUI::updateVessels(AISMessage *ais)
|
||||
QString name = nameItem->text();
|
||||
QString callsign = callsignItem->text();
|
||||
QString destination = destinationItem->text();
|
||||
QString shipType = shipTypeItem->text();
|
||||
QString status = statusItem->text();
|
||||
float heading = 0.0f;
|
||||
if (!name.isEmpty()) {
|
||||
text.append(QString("Name: %1").arg(name));
|
||||
}
|
||||
@ -549,16 +695,15 @@ void AISGUI::updateVessels(AISMessage *ais)
|
||||
{
|
||||
float course = courseV.toFloat();
|
||||
text.append(QString("Course: %1%2").arg(course).arg(QChar(0xb0)));
|
||||
swgMapItem->setImageRotation(course);
|
||||
heading = course;
|
||||
}
|
||||
if (!speedV.isNull()) {
|
||||
text.append(QString("Speed: %1 knts").arg(speedV.toFloat()));
|
||||
}
|
||||
if (!headingV.isNull())
|
||||
{
|
||||
float heading = headingV.toFloat();
|
||||
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));
|
||||
@ -566,10 +711,156 @@ void AISGUI::updateVessels(AISMessage *ais)
|
||||
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);
|
||||
// Send to map feature
|
||||
sendToMap(mmsiItem->text(), callsign,
|
||||
vessel->m_image, text.join("<br>"),
|
||||
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")
|
||||
{
|
||||
if (length < 40)
|
||||
{
|
||||
vessel->m_model = "ship_27m.glbe";
|
||||
}
|
||||
else if (length < 60)
|
||||
{
|
||||
vessel->m_model = "dredger_53m.glbe";
|
||||
}
|
||||
else
|
||||
{
|
||||
vessel->m_model = "ship_65m.glbe";
|
||||
}
|
||||
}
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,9 @@
|
||||
|
||||
#include <QTimer>
|
||||
#include <QMenu>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#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<QString, Vessel *> m_vessels; // Hash of mmsi to vessels
|
||||
static QStringList m_shipModels;
|
||||
static QStringList m_sailboatModels;
|
||||
static QHash<QString, float> m_labelOffset;
|
||||
static QHash<QString, float> 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_
|
||||
|
@ -30,7 +30,6 @@
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Liberation Sans</family>
|
||||
<pointsize>9</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
@ -166,11 +165,40 @@
|
||||
<string>Ship Type</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Length</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Destination</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Position Updated</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Time last position was received</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Updated</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Time last message was received</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Messages</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Number of messages received</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -27,7 +27,7 @@
|
||||
class Serializable;
|
||||
|
||||
// Number of columns in the tables
|
||||
#define AIS_VESSEL_COLUMNS 13
|
||||
#define AIS_VESSEL_COLUMNS 16
|
||||
|
||||
struct AISSettings
|
||||
{
|
||||
|
@ -2,9 +2,11 @@
|
||||
|
||||
<h2>Introduction</h2>
|
||||
|
||||
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.
|
||||
|
||||
<h2>Interface</h2>
|
||||
|
||||
@ -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.
|
||||
|
||||
<h3>Map</h3>
|
||||
|
||||
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)
|
||||
|
||||
<h2>Attribution</h2>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user