1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2024-12-23 01:55:48 -05:00

Add Map feature for displaying the combined items from other plugins

This commit is contained in:
Jon Beniston 2021-01-13 20:07:28 +00:00
parent c74ec2c426
commit bbe75aab6f
22 changed files with 2331 additions and 0 deletions

View File

@ -5,6 +5,7 @@ if (Qt5SerialPort_FOUND)
endif()
if (Qt5Quick_FOUND AND Qt5QuickWidgets_FOUND AND Qt5Positioning_FOUND)
add_subdirectory(map)
add_subdirectory(vorlocalizer)
endif()

View File

@ -0,0 +1,56 @@
project(map)
set(map_SOURCES
map.cpp
mapsettings.cpp
mapplugin.cpp
mapwebapiadapter.cpp
)
set(map_HEADERS
map.h
mapsettings.h
mapplugin.h
mapreport.h
mapwebapiadapter.h
)
include_directories(
${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client
)
if(NOT SERVER_MODE)
set(map_SOURCES
${map_SOURCES}
mapgui.cpp
mapgui.ui
map.qrc
)
set(map_HEADERS
${map_HEADERS}
mapgui.h
)
set(TARGET_NAME map)
set(TARGET_LIB "Qt5::Widgets" Qt5::Quick Qt5::QuickWidgets Qt5::Positioning)
set(TARGET_LIB_GUI "sdrgui")
set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR})
else()
set(TARGET_NAME mapsrv)
set(TARGET_LIB "")
set(TARGET_LIB_GUI "")
set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR})
endif()
add_library(${TARGET_NAME} SHARED
${map_SOURCES}
)
target_link_libraries(${TARGET_NAME}
Qt5::Core
${TARGET_LIB}
sdrbase
${TARGET_LIB_GUI}
)
install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER})

345
plugins/feature/map/map.cpp Normal file
View File

@ -0,0 +1,345 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QBuffer>
#include "SWGFeatureSettings.h"
#include "SWGFeatureReport.h"
#include "SWGFeatureActions.h"
#include "SWGDeviceState.h"
#include "dsp/dspengine.h"
#include "device/deviceset.h"
#include "channel/channelapi.h"
#include "feature/featureset.h"
#include "maincore.h"
#include "map.h"
MESSAGE_CLASS_DEFINITION(Map::MsgConfigureMap, Message)
MESSAGE_CLASS_DEFINITION(Map::MsgFind, Message)
const char* const Map::m_featureIdURI = "sdrangel.feature.map";
const char* const Map::m_featureId = "Map";
Map::Map(WebAPIAdapterInterface *webAPIAdapterInterface) :
Feature(m_featureIdURI, webAPIAdapterInterface)
{
qDebug("Map::Map: webAPIAdapterInterface: %p", webAPIAdapterInterface);
setObjectName(m_featureId);
m_state = StIdle;
m_errorMessage = "Map error";
connect(&m_updatePipesTimer, SIGNAL(timeout()), this, SLOT(updatePipes()));
m_updatePipesTimer.start(1000);
}
Map::~Map()
{
}
bool Map::handleMessage(const Message& cmd)
{
if (MsgConfigureMap::match(cmd))
{
MsgConfigureMap& cfg = (MsgConfigureMap&) cmd;
qDebug() << "Map::handleMessage: MsgConfigureMap";
applySettings(cfg.getSettings(), cfg.getForce());
return true;
}
else if (MainCore::MsgMapItem::match(cmd))
{
MainCore::MsgMapItem& msgMapItem = (MainCore::MsgMapItem&) cmd;
MainCore::MsgMapItem *copy = new MainCore::MsgMapItem(msgMapItem);
getMessageQueueToGUI()->push(copy);
return true;
}
else
{
return false;
}
}
void Map::updatePipes()
{
QList<AvailablePipeSource> availablePipes = updateAvailablePipeSources("mapitems", MapSettings::m_pipeTypes, MapSettings::m_pipeURIs, this);
if (availablePipes != m_availablePipes)
{
m_availablePipes = availablePipes;
if (getMessageQueueToGUI())
{
MsgReportPipes *msgToGUI = MsgReportPipes::create();
QList<AvailablePipeSource>& msgAvailablePipes = msgToGUI->getAvailablePipes();
msgAvailablePipes.append(availablePipes);
getMessageQueueToGUI()->push(msgToGUI);
}
}
}
QByteArray Map::serialize() const
{
return m_settings.serialize();
}
bool Map::deserialize(const QByteArray& data)
{
if (m_settings.deserialize(data))
{
MsgConfigureMap *msg = MsgConfigureMap::create(m_settings, true);
m_inputMessageQueue.push(msg);
return true;
}
else
{
m_settings.resetToDefaults();
MsgConfigureMap *msg = MsgConfigureMap::create(m_settings, true);
m_inputMessageQueue.push(msg);
return false;
}
}
void Map::applySettings(const MapSettings& settings, bool force)
{
qDebug() << "Map::applySettings:"
<< " m_displayNames: " << settings.m_displayNames
<< " m_title: " << settings.m_title
<< " m_rgbColor: " << settings.m_rgbColor
<< " m_useReverseAPI: " << settings.m_useReverseAPI
<< " m_reverseAPIAddress: " << settings.m_reverseAPIAddress
<< " m_reverseAPIPort: " << settings.m_reverseAPIPort
<< " m_reverseAPIFeatureSetIndex: " << settings.m_reverseAPIFeatureSetIndex
<< " m_reverseAPIFeatureIndex: " << settings.m_reverseAPIFeatureIndex
<< " force: " << force;
QList<QString> reverseAPIKeys;
if ((m_settings.m_displayNames != settings.m_displayNames) || force) {
reverseAPIKeys.append("displayNames");
}
if ((m_settings.m_title != settings.m_title) || force) {
reverseAPIKeys.append("title");
}
if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) {
reverseAPIKeys.append("rgbColor");
}
if (settings.m_useReverseAPI)
{
bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) ||
(m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) ||
(m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) ||
(m_settings.m_reverseAPIFeatureSetIndex != settings.m_reverseAPIFeatureSetIndex) ||
(m_settings.m_reverseAPIFeatureIndex != settings.m_reverseAPIFeatureIndex);
webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force);
}
m_settings = settings;
}
int Map::webapiRun(bool run,
SWGSDRangel::SWGDeviceState& response,
QString& errorMessage)
{
(void) errorMessage;
getFeatureStateStr(*response.getState());
return 202;
}
int Map::webapiSettingsGet(
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setMapSettings(new SWGSDRangel::SWGMapSettings());
response.getMapSettings()->init();
webapiFormatFeatureSettings(response, m_settings);
return 200;
}
int Map::webapiSettingsPutPatch(
bool force,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage)
{
(void) errorMessage;
MapSettings settings = m_settings;
webapiUpdateFeatureSettings(settings, featureSettingsKeys, response);
MsgConfigureMap *msg = MsgConfigureMap::create(settings, force);
m_inputMessageQueue.push(msg);
if (m_guiMessageQueue) // forward to GUI if any
{
MsgConfigureMap *msgToGUI = MsgConfigureMap::create(settings, force);
m_guiMessageQueue->push(msgToGUI);
}
webapiFormatFeatureSettings(response, settings);
return 200;
}
int Map::webapiActionsPost(
const QStringList& featureActionsKeys,
SWGSDRangel::SWGFeatureActions& query,
QString& errorMessage)
{
SWGSDRangel::SWGMapActions *swgMapActions = query.getMapActions();
if (swgMapActions)
{
if (featureActionsKeys.contains("find"))
{
QString id = *swgMapActions->getFind();
if (getMessageQueueToGUI())
getMessageQueueToGUI()->push(MsgFind::create(id));
}
return 202;
}
else
{
errorMessage = "Missing MapActions in query";
return 400;
}
}
void Map::webapiFormatFeatureSettings(
SWGSDRangel::SWGFeatureSettings& response,
const MapSettings& settings)
{
response.getMapSettings()->setDisplayNames(settings.m_displayNames ? 1 : 0);
if (response.getMapSettings()->getTitle()) {
*response.getMapSettings()->getTitle() = settings.m_title;
} else {
response.getMapSettings()->setTitle(new QString(settings.m_title));
}
response.getMapSettings()->setRgbColor(settings.m_rgbColor);
response.getMapSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0);
if (response.getMapSettings()->getReverseApiAddress()) {
*response.getMapSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress;
} else {
response.getMapSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress));
}
response.getMapSettings()->setReverseApiPort(settings.m_reverseAPIPort);
response.getMapSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIFeatureSetIndex);
response.getMapSettings()->setReverseApiChannelIndex(settings.m_reverseAPIFeatureIndex);
}
void Map::webapiUpdateFeatureSettings(
MapSettings& settings,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response)
{
if (featureSettingsKeys.contains("displayNames")) {
settings.m_displayNames = response.getMapSettings()->getDisplayNames();
}
if (featureSettingsKeys.contains("title")) {
settings.m_title = *response.getMapSettings()->getTitle();
}
if (featureSettingsKeys.contains("rgbColor")) {
settings.m_rgbColor = response.getMapSettings()->getRgbColor();
}
if (featureSettingsKeys.contains("useReverseAPI")) {
settings.m_useReverseAPI = response.getMapSettings()->getUseReverseApi() != 0;
}
if (featureSettingsKeys.contains("reverseAPIAddress")) {
settings.m_reverseAPIAddress = *response.getMapSettings()->getReverseApiAddress();
}
if (featureSettingsKeys.contains("reverseAPIPort")) {
settings.m_reverseAPIPort = response.getMapSettings()->getReverseApiPort();
}
if (featureSettingsKeys.contains("reverseAPIDeviceIndex")) {
settings.m_reverseAPIFeatureSetIndex = response.getMapSettings()->getReverseApiDeviceIndex();
}
if (featureSettingsKeys.contains("reverseAPIChannelIndex")) {
settings.m_reverseAPIFeatureIndex = response.getMapSettings()->getReverseApiChannelIndex();
}
}
void Map::webapiReverseSendSettings(QList<QString>& featureSettingsKeys, const MapSettings& settings, bool force)
{
SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings();
// swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet());
// swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex());
swgFeatureSettings->setFeatureType(new QString("Map"));
swgFeatureSettings->setMapSettings(new SWGSDRangel::SWGMapSettings());
SWGSDRangel::SWGMapSettings *swgMapSettings = swgFeatureSettings->getMapSettings();
// transfer data that has been modified. When force is on transfer all data except reverse API data
if (featureSettingsKeys.contains("displayNames") || force) {
swgMapSettings->setDisplayNames(settings.m_displayNames);
}
if (featureSettingsKeys.contains("title") || force) {
swgMapSettings->setTitle(new QString(settings.m_title));
}
if (featureSettingsKeys.contains("rgbColor") || force) {
swgMapSettings->setRgbColor(settings.m_rgbColor);
}
QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings")
.arg(settings.m_reverseAPIAddress)
.arg(settings.m_reverseAPIPort)
.arg(settings.m_reverseAPIFeatureSetIndex)
.arg(settings.m_reverseAPIFeatureIndex);
m_networkRequest.setUrl(QUrl(channelSettingsURL));
m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QBuffer *buffer = new QBuffer();
buffer->open((QBuffer::ReadWrite));
buffer->write(swgFeatureSettings->asJson().toUtf8());
buffer->seek(0);
// Always use PATCH to avoid passing reverse API settings
QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer);
buffer->setParent(reply);
delete swgFeatureSettings;
}
void Map::networkManagerFinished(QNetworkReply *reply)
{
QNetworkReply::NetworkError replyError = reply->error();
if (replyError)
{
qWarning() << "Map::networkManagerFinished:"
<< " error(" << (int) replyError
<< "): " << replyError
<< ": " << reply->errorString();
}
else
{
QString answer = reply->readAll();
answer.chop(1); // remove last \n
qDebug("Map::networkManagerFinished: reply:\n%s", answer.toStdString().c_str());
}
reply->deleteLater();
}

144
plugins/feature/map/map.h Normal file
View File

@ -0,0 +1,144 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MAP_H_
#define INCLUDE_FEATURE_MAP_H_
#include <QThread>
#include <QHash>
#include <QNetworkRequest>
#include <QTimer>
#include "feature/feature.h"
#include "util/message.h"
#include "mapsettings.h"
class WebAPIAdapterInterface;
class QNetworkAccessManager;
class QNetworkReply;
namespace SWGSDRangel {
class SWGDeviceState;
}
class Map : public Feature
{
Q_OBJECT
public:
class MsgConfigureMap : public Message {
MESSAGE_CLASS_DECLARATION
public:
const MapSettings& getSettings() const { return m_settings; }
bool getForce() const { return m_force; }
static MsgConfigureMap* create(const MapSettings& settings, bool force) {
return new MsgConfigureMap(settings, force);
}
private:
MapSettings m_settings;
bool m_force;
MsgConfigureMap(const MapSettings& settings, bool force) :
Message(),
m_settings(settings),
m_force(force)
{ }
};
class MsgFind : public Message {
MESSAGE_CLASS_DECLARATION
public:
QString getTarget() const { return m_target; }
static MsgFind* create(const QString& target) {
return new MsgFind(target);
}
private:
QString m_target;
MsgFind(const QString& target) :
Message(),
m_target(target)
{}
};
Map(WebAPIAdapterInterface *webAPIAdapterInterface);
virtual ~Map();
virtual void destroy() { delete this; }
virtual bool handleMessage(const Message& cmd);
virtual void getIdentifier(QString& id) const { id = objectName(); }
virtual void getTitle(QString& title) const { title = m_settings.m_title; }
virtual QByteArray serialize() const;
virtual bool deserialize(const QByteArray& data);
virtual int webapiRun(bool run,
SWGSDRangel::SWGDeviceState& response,
QString& errorMessage);
virtual int webapiSettingsGet(
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage);
virtual int webapiActionsPost(
const QStringList& featureActionsKeys,
SWGSDRangel::SWGFeatureActions& query,
QString& errorMessage);
static void webapiFormatFeatureSettings(
SWGSDRangel::SWGFeatureSettings& response,
const MapSettings& settings);
static void webapiUpdateFeatureSettings(
MapSettings& settings,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response);
static const char* const m_featureIdURI;
static const char* const m_featureId;
private:
QThread m_thread;
MapSettings m_settings;
QList<AvailablePipeSource> m_availablePipes;
QTimer m_updatePipesTimer;
QNetworkAccessManager *m_networkManager;
QNetworkRequest m_networkRequest;
void applySettings(const MapSettings& settings, bool force = false);
void webapiReverseSendSettings(QList<QString>& featureSettingsKeys, const MapSettings& settings, bool force);
private slots:
void updatePipes();
void networkManagerFinished(QNetworkReply *reply);
};
#endif // INCLUDE_FEATURE_MAP_H_

View File

@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/map/">
<file>map/map.qml</file>
<file>map/MapStation.qml</file>
<file>map/antenna.png</file>
</qresource>
</RCC>

View File

@ -0,0 +1,40 @@
import QtQuick 2.12
import QtLocation 5.12
import QtPositioning 5.12
MapQuickItem {
id: station
property string stationName // Name of the station, E.g. Home
coordinate: QtPositioning.coordinate(51.5, 0.125) // Location of the antenna (QTH) - London
zoomLevel: 11
anchorPoint.x: image.width/2
anchorPoint.y: image.height/2
sourceItem: Grid {
columns: 1
Grid {
horizontalItemAlignment: Grid.AlignHCenter
layer.enabled: true
layer.smooth: true
Image {
id: image
source: "antenna.png"
}
Rectangle {
id: bubble
color: "lightblue"
border.width: 1
width: text.width * 1.3
height: text.height * 1.3
radius: 5
Text {
id: text
anchors.centerIn: parent
text: stationName
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,102 @@
import QtQuick 2.12
import QtQuick.Window 2.12
import QtLocation 5.12
import QtPositioning 5.12
Item {
id: qmlMap
property int mapZoomLevel: 11
Plugin {
id: mapPlugin
name: "osm"
}
Map {
id: map
objectName: "map"
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(51.5, 0.125) // London
zoomLevel: 10
MapItemView {
model: mapModel
delegate: mapComponent
}
MapStation {
id: station
objectName: "station"
stationName: "Home"
coordinate: QtPositioning.coordinate(51.5, 0.125)
}
onZoomLevelChanged: {
if (zoomLevel > 11) {
station.zoomLevel = zoomLevel
mapZoomLevel = zoomLevel
} else {
station.zoomLevel = 11
mapZoomLevel = 11
}
}
}
Component {
id: mapComponent
MapQuickItem {
id: mapElement
anchorPoint.x: image.width/2
anchorPoint.y: image.height/2
coordinate: position
zoomLevel: mapImageFixedSize ? zoomLevel : mapZoomLevel
sourceItem: Grid {
columns: 1
Grid {
horizontalItemAlignment: Grid.AlignHCenter
columnSpacing: 5
layer.enabled: true
layer.smooth: true
Image {
id: image
rotation: mapImageRotation
source: mapImage
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: (mouse) => {
selected = !selected
}
}
}
Rectangle {
id: bubble
color: bubbleColour
border.width: 1
width: text.width + 5
height: text.height + 5
radius: 5
visible: mapTextVisible
Text {
id: text
anchors.centerIn: parent
text: mapText
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: (mouse) => {
selected = !selected
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,382 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <cmath>
#include <QMessageBox>
#include <QLineEdit>
#include <QQmlContext>
#include <QQuickItem>
#include <QGeoLocation>
#include <QGeoCoordinate>
#include "feature/featureuiset.h"
#include "gui/basicfeaturesettingsdialog.h"
#include "mainwindow.h"
#include "device/deviceuiset.h"
#include "util/units.h"
#include "util/maidenhead.h"
#include "ui_mapgui.h"
#include "map.h"
#include "mapgui.h"
#include "SWGMapItem.h"
QVariant MapModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if ((row < 0) || (row >= m_items.count()))
return QVariant();
if (role == MapModel::positionRole)
{
// Coordinates to display the item at
QGeoCoordinate coords;
coords.setLatitude(m_items[row]->m_latitude);
coords.setLongitude(m_items[row]->m_longitude);
return QVariant::fromValue(coords);
}
else if (role == MapModel::mapTextRole)
{
// Create the text to go in the bubble next to the image
if (m_selected[row])
return QVariant::fromValue(m_items[row]->m_text);
else
return QVariant::fromValue(m_items[row]->m_name);
}
else if (role == MapModel::mapTextVisibleRole)
{
return QVariant::fromValue(m_selected[row] || m_displayNames);
}
else if (role == MapModel::mapImageRole)
{
// Set an image to use
return QVariant::fromValue(m_items[row]->m_image);
}
else if (role == MapModel::mapImageRotationRole)
{
// Angle to rotate image by
return QVariant::fromValue(m_items[row]->m_imageRotation);
}
else if (role == MapModel::mapImageFixedSizeRole)
{
// Whether image changes size with zoom level
return QVariant::fromValue(m_items[row]->m_imageFixedSize);
}
else if (role == MapModel::bubbleColourRole)
{
// Select a background colour for the text bubble next to the item
if (m_selected[row])
return QVariant::fromValue(QColor("lightgreen"));
else
return QVariant::fromValue(QColor("lightblue"));
}
else if (role == MapModel::selectedRole)
return QVariant::fromValue(m_selected[row]);
return QVariant();
}
bool MapModel::setData(const QModelIndex &index, const QVariant& value, int role)
{
int row = index.row();
if ((row < 0) || (row >= m_items.count()))
return false;
if (role == MapModel::selectedRole)
{
m_selected[row] = value.toBool();
emit dataChanged(index, index);
return true;
}
return true;
}
MapGUI* MapGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature)
{
MapGUI* gui = new MapGUI(pluginAPI, featureUISet, feature);
return gui;
}
void MapGUI::destroy()
{
delete this;
}
void MapGUI::resetToDefaults()
{
m_settings.resetToDefaults();
displaySettings();
applySettings(true);
}
QByteArray MapGUI::serialize() const
{
return m_settings.serialize();
}
bool MapGUI::deserialize(const QByteArray& data)
{
if (m_settings.deserialize(data))
{
displaySettings();
applySettings(true);
return true;
}
else
{
resetToDefaults();
return false;
}
}
bool MapGUI::handleMessage(const Message& message)
{
if (Map::MsgConfigureMap::match(message))
{
qDebug("MapGUI::handleMessage: Map::MsgConfigureMap");
const Map::MsgConfigureMap& cfg = (Map::MsgConfigureMap&) message;
m_settings = cfg.getSettings();
blockApplySettings(true);
displaySettings();
blockApplySettings(false);
return true;
}
else if (PipeEndPoint::MsgReportPipes::match(message))
{
PipeEndPoint::MsgReportPipes& report = (PipeEndPoint::MsgReportPipes&) message;
m_availablePipes = report.getAvailablePipes();
updatePipeList();
return true;
}
else if (Map::MsgFind::match(message))
{
Map::MsgFind& msgFind = (Map::MsgFind&) message;
find(msgFind.getTarget());
return true;
}
else if (MainCore::MsgMapItem::match(message))
{
MainCore::MsgMapItem& msgMapItem = (MainCore::MsgMapItem&) message;
SWGSDRangel::SWGMapItem *swgMapItem = msgMapItem.getSWGMapItem();
m_mapModel.update(msgMapItem.getPipeSource(), swgMapItem);
return true;
}
return false;
}
void MapGUI::handleInputMessages()
{
Message* message;
while ((message = getInputMessageQueue()->pop()))
{
if (handleMessage(*message)) {
delete message;
}
}
}
void MapGUI::onWidgetRolled(QWidget* widget, bool rollDown)
{
(void) widget;
(void) rollDown;
}
MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) :
FeatureGUI(parent),
ui(new Ui::MapGUI),
m_pluginAPI(pluginAPI),
m_featureUISet(featureUISet),
m_doApplySettings(true),
m_mapModel(this)
{
ui->setupUi(this);
ui->map->rootContext()->setContextProperty("mapModel", &m_mapModel);
ui->map->setSource(QUrl(QStringLiteral("qrc:/map/map/map.qml")));
setAttribute(Qt::WA_DeleteOnClose, true);
setChannelWidget(false);
connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool)));
m_map = reinterpret_cast<Map*>(feature);
m_map->setMessageQueueToGUI(&m_inputMessageQueue);
m_featureUISet->addRollupWidget(this);
// Get station position
float stationLatitude = MainCore::instance()->getSettings().getLatitude();
float stationLongitude = MainCore::instance()->getSettings().getLongitude();
float stationAltitude = MainCore::instance()->getSettings().getAltitude();
// Centre map at My Position
QQuickItem *item = ui->map->rootObject();
QObject *object = item->findChild<QObject*>("map");
if(object != NULL)
{
QGeoCoordinate coords = object->property("center").value<QGeoCoordinate>();
coords.setLatitude(stationLatitude);
coords.setLongitude(stationLongitude);
object->setProperty("center", QVariant::fromValue(coords));
}
// Move antenna icon to My Position to start with
QObject *stationObject = item->findChild<QObject*>("station");
if(stationObject != NULL)
{
QGeoCoordinate coords = stationObject->property("coordinate").value<QGeoCoordinate>();
coords.setLatitude(stationLatitude);
coords.setLongitude(stationLongitude);
coords.setAltitude(stationAltitude);
stationObject->setProperty("coordinate", QVariant::fromValue(coords));
stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName()));
}
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &)));
connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages()));
displaySettings();
applySettings(true);
}
MapGUI::~MapGUI()
{
delete ui;
}
void MapGUI::blockApplySettings(bool block)
{
m_doApplySettings = !block;
}
void MapGUI::displaySettings()
{
setTitleColor(m_settings.m_rgbColor);
setWindowTitle(m_settings.m_title);
blockApplySettings(true);
ui->displayNames->setChecked(m_settings.m_displayNames);
m_mapModel.setDisplayNames(m_settings.m_displayNames);
blockApplySettings(false);
}
void MapGUI::updatePipeList()
{
ui->pipes->blockSignals(true);
ui->pipes->clear();
QList<PipeEndPoint::AvailablePipeSource>::const_iterator it = m_availablePipes.begin();
for (int i = 0; it != m_availablePipes.end(); ++it, i++)
{
ui->pipes->addItem(it->getName());
}
ui->pipes->blockSignals(false);
}
void MapGUI::leaveEvent(QEvent*)
{
}
void MapGUI::enterEvent(QEvent*)
{
}
void MapGUI::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 MapGUI::applySettings(bool force)
{
if (m_doApplySettings)
{
Map::MsgConfigureMap* message = Map::MsgConfigureMap::create(m_settings, force);
m_map->getInputMessageQueue()->push(message);
}
}
void MapGUI::on_displayNames_clicked(bool checked)
{
m_settings.m_displayNames = checked;
m_mapModel.setDisplayNames(checked);
}
void MapGUI::on_find_returnPressed()
{
find(ui->find->text().trimmed());
}
void MapGUI::find(const QString& target)
{
if (!target.isEmpty())
{
QQuickItem *item = ui->map->rootObject();
QObject *map = item->findChild<QObject*>("map");
if (map != NULL)
{
float latitude, longitude;
if (Units::stringToLatitudeAndLongitude(target, latitude, longitude))
{
map->setProperty("center", QVariant::fromValue(QGeoCoordinate(latitude, longitude)));
}
else if (Maidenhead::fromMaidenhead(target, latitude, longitude))
{
map->setProperty("center", QVariant::fromValue(QGeoCoordinate(latitude, longitude)));
}
else
{
MapItem *mapItem = m_mapModel.findMapItem(target);
if (mapItem != nullptr)
map->setProperty("center", QVariant::fromValue(mapItem->getCoordinates()));
}
}
}
}
void MapGUI::on_deleteAll_clicked()
{
m_mapModel.removeAll();
}

View File

@ -0,0 +1,304 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MAPGUI_H_
#define INCLUDE_FEATURE_MAPGUI_H_
#include <QTimer>
#include <QAbstractListModel>
#include <QGeoCoordinate>
#include "feature/featuregui.h"
#include "util/messagequeue.h"
#include "pipes/pipeendpoint.h"
#include "mapsettings.h"
#include "SWGMapItem.h"
class PluginAPI;
class FeatureUISet;
class Map;
namespace Ui {
class MapGUI;
}
class MapGUI;
class MapModel;
// Information required about each item displayed on the map
class MapItem {
public:
MapItem(const PipeEndPoint *source, SWGSDRangel::SWGMapItem *mapItem)
{
m_source = source;
m_name = *mapItem->getName();
m_latitude = mapItem->getLatitude();
m_longitude = mapItem->getLongitude();
m_image = *mapItem->getImage();
m_imageRotation = mapItem->getImageRotation();
m_imageFixedSize = mapItem->getImageFixedSize() == 1;
m_text = *mapItem->getText();
}
void update(SWGSDRangel::SWGMapItem *mapItem)
{
m_latitude = mapItem->getLatitude();
m_longitude = mapItem->getLongitude();
m_image = *mapItem->getImage();
m_imageRotation = mapItem->getImageRotation();
m_imageFixedSize = mapItem->getImageFixedSize() == 1;
m_text = *mapItem->getText();
}
QGeoCoordinate getCoordinates()
{
QGeoCoordinate coords;
coords.setLatitude(m_latitude);
coords.setLongitude(m_longitude);
return coords;
}
private:
friend MapModel;
const PipeEndPoint *m_source; // Channel/feature that created the item
QString m_name;
float m_latitude;
float m_longitude;
QString m_image;
int m_imageRotation;
bool m_imageFixedSize; // Keep image same size when map is zoomed
QString m_text;
};
// Model used for each item on the map
class MapModel : public QAbstractListModel {
Q_OBJECT
public:
using QAbstractListModel::QAbstractListModel;
enum MarkerRoles {
positionRole = Qt::UserRole + 1,
mapTextRole = Qt::UserRole + 2,
mapTextVisibleRole = Qt::UserRole + 3,
mapImageRole = Qt::UserRole + 4,
mapImageRotationRole = Qt::UserRole + 5,
mapImageFixedSizeRole = Qt::UserRole + 6,
bubbleColourRole = Qt::UserRole + 7,
selectedRole = Qt::UserRole + 8
};
MapModel(MapGUI *gui) :
m_gui(gui)
{
}
Q_INVOKABLE void add(MapItem *item)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_items.append(item);
m_selected.append(false);
endInsertRows();
}
void update(const PipeEndPoint *source, SWGSDRangel::SWGMapItem *swgMapItem)
{
QString name = *swgMapItem->getName();
// Add, update or delete and item
MapItem *item = findMapItem(source, name);
if (item != nullptr)
{
QString image = *swgMapItem->getImage();
if (image.isEmpty())
{
// Delete the item
remove(item);
}
else
{
// Update the item
item->update(swgMapItem);
update(item);
}
}
else
{
// Make sure not a duplicate request to delete
QString image = *swgMapItem->getImage();
if (!image.isEmpty())
{
// Add new item
add(new MapItem(source, swgMapItem));
}
}
}
void update(MapItem *item)
{
int row = m_items.indexOf(item);
if (row >= 0)
{
QModelIndex idx = index(row);
emit dataChanged(idx, idx);
}
}
void remove(MapItem *item)
{
int row = m_items.indexOf(item);
if (row >= 0)
{
beginRemoveRows(QModelIndex(), row, row);
m_items.removeAt(row);
m_selected.removeAt(row);
endRemoveRows();
}
}
MapItem *findMapItem(const PipeEndPoint *source, const QString& name)
{
// FIXME: Should consider adding a QHash for this
QListIterator<MapItem *> i(m_items);
while (i.hasNext())
{
MapItem *item = i.next();
if ((item->m_name == name) && (item->m_source == source))
return item;
}
return nullptr;
}
MapItem *findMapItem(const QString& name)
{
QListIterator<MapItem *> i(m_items);
while (i.hasNext())
{
MapItem *item = i.next();
if (item->m_name == name)
return item;
}
return nullptr;
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent)
return m_items.count();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override
{
(void) index;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
void allUpdated()
{
for (int i = 0; i < m_items.count(); i++)
{
QModelIndex idx = index(i);
emit dataChanged(idx, idx);
}
}
void removeAll()
{
beginRemoveRows(QModelIndex(), 0, m_items.count());
m_items.clear();
m_selected.clear();
endRemoveRows();
}
void setDisplayNames(bool displayNames)
{
m_displayNames = displayNames;
allUpdated();
}
QHash<int, QByteArray> roleNames() const
{
QHash<int, QByteArray> roles;
roles[positionRole] = "position";
roles[mapTextRole] = "mapText";
roles[mapTextVisibleRole] = "mapTextVisible";
roles[mapImageRole] = "mapImage";
roles[mapImageRotationRole] = "mapImageRotation";
roles[mapImageFixedSizeRole] = "mapImageFixedSize";
roles[bubbleColourRole] = "bubbleColour";
roles[selectedRole] = "selected";
return roles;
}
private:
MapGUI *m_gui;
QList<MapItem *> m_items;
QList<bool> m_selected;
bool m_displayNames;
};
class MapGUI : public FeatureGUI {
Q_OBJECT
public:
static MapGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature);
virtual void destroy();
void resetToDefaults();
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; }
private:
Ui::MapGUI* ui;
PluginAPI* m_pluginAPI;
FeatureUISet* m_featureUISet;
MapSettings m_settings;
bool m_doApplySettings;
QList<PipeEndPoint::AvailablePipeSource> m_availablePipes;
Map* m_map;
MessageQueue m_inputMessageQueue;
MapModel m_mapModel;
explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr);
virtual ~MapGUI();
void blockApplySettings(bool block);
void applySettings(bool force = false);
void displaySettings();
void updatePipeList();
bool handleMessage(const Message& message);
void find(const QString& target);
void leaveEvent(QEvent*);
void enterEvent(QEvent*);
private slots:
void onMenuDialogCalled(const QPoint &p);
void onWidgetRolled(QWidget* widget, bool rollDown);
void handleInputMessages();
void on_displayNames_clicked(bool checked=false);
void on_find_returnPressed();
void on_deleteAll_clicked();
};
#endif // INCLUDE_FEATURE_MAPGUI_H_

View File

@ -0,0 +1,250 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MapGUI</class>
<widget class="RollupWidget" name="MapGUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>462</width>
<height>689</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>462</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<family>Liberation Sans</family>
<pointsize>9</pointsize>
</font>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="windowTitle">
<string>Map</string>
</property>
<property name="statusTip">
<string>Map</string>
</property>
<widget class="QWidget" name="settingsContainer" native="true">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>461</width>
<height>41</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>350</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>3</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QLabel" name="pipesLabel">
<property name="text">
<string>Sources</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="pipes">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Data source channels and features</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="findLabel">
<property name="text">
<string>Find</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="find">
<property name="toolTip">
<string>Enter name of object to find, latitude and longitude or Maidenhead locator</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="ButtonSwitch" name="displayNames">
<property name="toolTip">
<string>Display names</string>
</property>
<property name="text">
<string>^</string>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/info.png</normaloff>:/info.png</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deleteAll">
<property name="toolTip">
<string>Delete all items on the map</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../../../sdrgui/resources/res.qrc">
<normaloff>:/bin.png</normaloff>:/bin.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="mapContainer" native="true">
<property name="geometry">
<rect>
<x>0</x>
<y>100</y>
<width>461</width>
<height>581</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Map</string>
</property>
<layout class="QVBoxLayout" name="verticalLayoutMap" stretch="0">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
<widget class="QQuickWidget" name="map">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>500</height>
</size>
</property>
<property name="toolTip">
<string>Map</string>
</property>
<property name="resizeMode">
<enum>QQuickWidget::SizeRootObjectToView</enum>
</property>
<property name="source">
<url>
<string/>
</url>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>QQuickWidget</class>
<extends>QWidget</extends>
<header location="global">QtQuickWidgets/QQuickWidget</header>
</customwidget>
<customwidget>
<class>RollupWidget</class>
<extends>QWidget</extends>
<header>gui/rollupwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>map</tabstop>
</tabstops>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,80 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QtPlugin>
#include "plugin/pluginapi.h"
#ifndef SERVER_MODE
#include "mapgui.h"
#endif
#include "map.h"
#include "mapplugin.h"
#include "mapwebapiadapter.h"
const PluginDescriptor MapPlugin::m_pluginDescriptor = {
Map::m_featureId,
QStringLiteral("Map"),
QStringLiteral("6.4.0"),
QStringLiteral("(c) Jon Beniston, M7RCE"),
QStringLiteral("https://github.com/f4exb/sdrangel"),
true,
QStringLiteral("https://github.com/f4exb/sdrangel")
};
MapPlugin::MapPlugin(QObject* parent) :
QObject(parent),
m_pluginAPI(nullptr)
{
}
const PluginDescriptor& MapPlugin::getPluginDescriptor() const
{
return m_pluginDescriptor;
}
void MapPlugin::initPlugin(PluginAPI* pluginAPI)
{
m_pluginAPI = pluginAPI;
m_pluginAPI->registerFeature(Map::m_featureIdURI, Map::m_featureId, this);
}
#ifdef SERVER_MODE
FeatureGUI* MapPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const
{
(void) featureUISet;
(void) feature;
return nullptr;
}
#else
FeatureGUI* MapPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const
{
return MapGUI::create(m_pluginAPI, featureUISet, feature);
}
#endif
Feature* MapPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const
{
return new Map(webAPIAdapterInterface);
}
FeatureWebAPIAdapter* MapPlugin::createFeatureWebAPIAdapter() const
{
return new MapWebAPIAdapter();
}

View File

@ -0,0 +1,49 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MAPPLUGIN_H
#define INCLUDE_FEATURE_MAPPLUGIN_H
#include <QObject>
#include "plugin/plugininterface.h"
class FeatureGUI;
class WebAPIAdapterInterface;
class MapPlugin : public QObject, PluginInterface {
Q_OBJECT
Q_INTERFACES(PluginInterface)
Q_PLUGIN_METADATA(IID "sdrangel.feature.map")
public:
explicit MapPlugin(QObject* parent = nullptr);
const PluginDescriptor& getPluginDescriptor() const;
void initPlugin(PluginAPI* pluginAPI);
virtual FeatureGUI* createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const;
virtual Feature* createFeature(WebAPIAdapterInterface *webAPIAdapterInterface) const;
virtual FeatureWebAPIAdapter* createFeatureWebAPIAdapter() const;
private:
static const PluginDescriptor m_pluginDescriptor;
PluginAPI* m_pluginAPI;
};
#endif // INCLUDE_FEATURE_MAPPLUGIN_H

View File

@ -0,0 +1,112 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QColor>
#include "util/simpleserializer.h"
#include "settings/serializable.h"
#include "mapsettings.h"
const QStringList MapSettings::m_pipeTypes = {
QStringLiteral("ADSBDemod"),
QStringLiteral("APRS"),
QStringLiteral("StarTracker")
};
const QStringList MapSettings::m_pipeURIs = {
QStringLiteral("sdrangel.channel.adsbdemod"),
QStringLiteral("sdrangel.feature.aprs"),
QStringLiteral("sdrangel.feature.startracker")
};
MapSettings::MapSettings()
{
resetToDefaults();
}
void MapSettings::resetToDefaults()
{
m_displayNames = true;
m_title = "Map";
m_rgbColor = QColor(225, 25, 99).rgb();
m_useReverseAPI = false;
m_reverseAPIAddress = "127.0.0.1";
m_reverseAPIPort = 8888;
m_reverseAPIFeatureSetIndex = 0;
m_reverseAPIFeatureIndex = 0;
}
QByteArray MapSettings::serialize() const
{
SimpleSerializer s(1);
s.writeBool(1, m_displayNames);
s.writeString(8, m_title);
s.writeU32(9, m_rgbColor);
s.writeBool(10, m_useReverseAPI);
s.writeString(11, m_reverseAPIAddress);
s.writeU32(12, m_reverseAPIPort);
s.writeU32(13, m_reverseAPIFeatureSetIndex);
s.writeU32(14, m_reverseAPIFeatureIndex);
return s.final();
}
bool MapSettings::deserialize(const QByteArray& data)
{
SimpleDeserializer d(data);
if (!d.isValid())
{
resetToDefaults();
return false;
}
if (d.getVersion() == 1)
{
QByteArray bytetmp;
uint32_t utmp;
QString strtmp;
d.readBool(1, &m_displayNames, true);
d.readString(8, &m_title, "Map");
d.readU32(9, &m_rgbColor, QColor(225, 25, 99).rgb());
d.readBool(10, &m_useReverseAPI, false);
d.readString(11, &m_reverseAPIAddress, "127.0.0.1");
d.readU32(12, &utmp, 0);
if ((utmp > 1023) && (utmp < 65535)) {
m_reverseAPIPort = utmp;
} else {
m_reverseAPIPort = 8888;
}
d.readU32(13, &utmp, 0);
m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp;
d.readU32(14, &utmp, 0);
m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp;
return true;
}
else
{
resetToDefaults();
return false;
}
}

View File

@ -0,0 +1,63 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_FEATURE_MAPSETTINGS_H_
#define INCLUDE_FEATURE_MAPSETTINGS_H_
#include <QByteArray>
#include <QString>
#include "util/message.h"
class Serializable;
class PipeEndPoint;
struct MapSettings
{
struct AvailablePipe
{
enum {RX, TX, Feature} m_type;
int m_setIndex;
int m_index;
PipeEndPoint *m_source;
QString m_id;
AvailablePipe() = default;
AvailablePipe(const AvailablePipe&) = default;
AvailablePipe& operator=(const AvailablePipe&) = default;
};
bool m_displayNames;
QString m_title;
quint32 m_rgbColor;
bool m_useReverseAPI;
QString m_reverseAPIAddress;
uint16_t m_reverseAPIPort;
uint16_t m_reverseAPIFeatureSetIndex;
uint16_t m_reverseAPIFeatureIndex;
MapSettings();
void resetToDefaults();
QByteArray serialize() const;
bool deserialize(const QByteArray& data);
static const QStringList m_pipeTypes;
static const QStringList m_pipeURIs;
};
#endif // INCLUDE_FEATURE_MAPSETTINGS_H_

View File

@ -0,0 +1,52 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "SWGFeatureSettings.h"
#include "map.h"
#include "mapwebapiadapter.h"
MapWebAPIAdapter::MapWebAPIAdapter()
{}
MapWebAPIAdapter::~MapWebAPIAdapter()
{}
int MapWebAPIAdapter::webapiSettingsGet(
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage)
{
(void) errorMessage;
response.setSimplePttSettings(new SWGSDRangel::SWGSimplePTTSettings());
response.getSimplePttSettings()->init();
Map::webapiFormatFeatureSettings(response, m_settings);
return 200;
}
int MapWebAPIAdapter::webapiSettingsPutPatch(
bool force,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage)
{
(void) force; // no action
(void) errorMessage;
Map::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response);
return 200;
}

View File

@ -0,0 +1,50 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MAP_WEBAPIADAPTER_H
#define INCLUDE_MAP_WEBAPIADAPTER_H
#include "feature/featurewebapiadapter.h"
#include "mapsettings.h"
/**
* Standalone API adapter only for the settings
*/
class MapWebAPIAdapter : public FeatureWebAPIAdapter {
public:
MapWebAPIAdapter();
virtual ~MapWebAPIAdapter();
virtual QByteArray serialize() const { return m_settings.serialize(); }
virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); }
virtual int webapiSettingsGet(
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage);
virtual int webapiSettingsPutPatch(
bool force,
const QStringList& featureSettingsKeys,
SWGSDRangel::SWGFeatureSettings& response,
QString& errorMessage);
private:
MapSettings m_settings;
};
#endif // INCLUDE_MAP_WEBAPIADAPTER_H

View File

@ -0,0 +1,43 @@
<h1>Map Feature Plugin</h1>
<h2>Introduction</h2>
The Map Feature plugin displays a world map. On top of this, it can plot data from other plugins, such as APRS symbols from the APRS Feature, aircraft from the ADS-B Demodulator or the Sun, Moon and Stars from the Star Tracker.
<h2>Interface</h2>
![Map feature plugin GUI](../../../doc/img/Map_plugin.png)
<h3>1: Source Channels</h3>
This displays the list of channels the Map is displaying data from.
<h3>2: Find</h3>
To centre the map on an object or location, enter:
* An object name.
* Latitude and longitude. This can be in decimal degrees (E.g: -23.666413, -46.573550) or degrees, minutes and seconds (E.g: 50°40'46.461"N 95°48'26.533"W or 33d51m54.5148sS 151d12m35.6400sE).
* A Maidenhead locator (E.g: IO86av).
<h3>3: Display Names<h3>
When checked, names of objects are displayed in a bubble next to each object.
<h3>Map</h3>
The map displays objects reported by other SDRangel channels and features.
* The antenna location is placed according to My Position set under the Preferences > My Position menu. The position is only updated when the Map plugin is first opened.
* To pan around the map, click the left mouse button and drag. To zoom in or out, use the mouse scroll wheel.
* Clicking on an object in the map will display a text bubble with additional information about the object.
<h2>API</h2>
Full details of the API can be found in the Swagger documentation. Here is a quick example of how to centre the map on an object from the command line:
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "M7RCE" }}'
And to centre the map at a particular latitude and longitude:
curl -X POST "http://127.0.0.1:8091/sdrangel/featureset/0/feature/0/actions" -d '{"featureType": "Map", "MapActions": { "find": "51.2 0.0" }}'

View File

@ -0,0 +1,101 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QDebug>
#include "SWGFeatureActions.h"
#include "SWGMapActions.h"
#include "maincore.h"
#include "feature/featureset.h"
#include "feature/feature.h"
#include "featurewebapiutils.h"
// Find the specified target on the map
bool FeatureWebAPIUtils::mapFind(const QString& target, int featureSetIndex, int featureIndex)
{
Feature *feature = FeatureWebAPIUtils::getFeature(featureSetIndex, featureIndex, "sdrangel.feature.map");
if (feature != nullptr)
{
QString errorMessage;
QStringList featureActionKeys = {"find"};
SWGSDRangel::SWGFeatureActions query;
SWGSDRangel::SWGMapActions *mapActions = new SWGSDRangel::SWGMapActions();
mapActions->setFind(new QString(target));
query.setMapActions(mapActions);
int httpRC = feature->webapiActionsPost(featureActionKeys, query, errorMessage);
if (httpRC/100 != 2)
{
qWarning("FeatureWebAPIUtils::mapFind: error %d: %s", httpRC, errorMessage);
return false;
}
return true;
}
else
{
qWarning("FeatureWebAPIUtils::mapFind: no Map feature");
return false;
}
}
// Get first feature with the given URI
Feature* FeatureWebAPIUtils::getFeature(int featureSetIndex, int featureIndex, const QString& uri)
{
FeatureSet *featureSet;
Feature *feature;
std::vector<FeatureSet*>& featureSets = MainCore::instance()->getFeatureeSets();
if (featureSetIndex != -1)
{
// Find feature with specific index
if (featureSetIndex < featureSets.size())
{
featureSet = featureSets[featureSetIndex];
if (featureIndex < featureSet->getNumberOfFeatures())
{
feature = featureSet->getFeatureAt(featureIndex);
if (uri.isEmpty() || feature->getURI() == uri)
return feature;
else
return nullptr;
}
else
return nullptr;
}
else
return nullptr;
}
else
{
// Find first feature matching URI
for (std::vector<FeatureSet*>::const_iterator it = featureSets.begin(); it != featureSets.end(); ++it, featureIndex++)
{
for (int fi = 0; fi < (*it)->getNumberOfFeatures(); fi++)
{
feature = (*it)->getFeatureAt(fi);
if (feature->getURI() == uri)
{
return feature;
}
}
}
return nullptr;
}
}

View File

@ -0,0 +1,32 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef SDRBASE_FEATURE_FEATUREWEBAPIUTILS_H_
#define SDRBASE_FEATURE_FEATUREWEBAPIUTILS_H_
#include "export.h"
class Feature;
class SDRBASE_API FeatureWebAPIUtils
{
public:
static bool mapFind(const QString& target, int featureSetIndex=-1, int featureIndex=-1);
static Feature *getFeature(int featureSetIndex, int featureIndex, const QString& uri);
};
#endif // SDRBASE_FEATURE_FEATUREWEBAPIUTILS_H_

View File

@ -0,0 +1,81 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// //
// Maidenhead locator utilities //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include <QRegExp>
#include "maidenhead.h"
// See: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
QString Maidenhead::toMaidenhead(float latitude, float longitude)
{
longitude += 180.0;
latitude += 90.0;
int lon1 = floor(longitude/20);
longitude -= lon1*20;
int lat1 = floor(latitude/10);
latitude -= lat1*10;
int lon2 = floor(longitude/2);
longitude -= lon2*2;
int lat2 = floor(latitude/1);
latitude -= lat2;
int lon3 = round(longitude*12);
int lat3 = round(latitude*24);
return QString("%1%2%3%4%5%6").arg(QChar(lon1+'A')).arg(QChar(lat1+'A'))
.arg(QChar(lon2+'0')).arg(QChar(lat2+'0'))
.arg(QChar(lon3+'A')).arg(QChar(lat3+'A'));
}
bool Maidenhead::fromMaidenhead(const QString& maidenhead, float& latitude, float& longitude)
{
if (Maidenhead::isMaidenhead(maidenhead))
{
int lon1 = maidenhead[0].toUpper().toLatin1() - 'A';
int lat1 = maidenhead[1].toUpper().toLatin1() - 'A';
int lon2 = maidenhead[2].toLatin1() - '0';
int lat2 = maidenhead[3].toLatin1() - '0';
int lon3 = maidenhead[4].toUpper().toLatin1() - 'A';
int lat3 = maidenhead[5].toUpper().toLatin1() - 'A';
int lon4 = 0;
int lat4 = 0;
if (maidenhead.length() == 8)
{
lon4 = maidenhead[6].toLatin1() - '0';
lat4 = maidenhead[7].toLatin1() - '0';
}
longitude = lon1 * 20 + lon2 * 2 + lon3 * 2.0/24.0 + lon4 * 2.0/24.0/10; // 20=360/18
latitude = lat1 * 10 + lat2 * 1 + lat3 * 1.0/24.0 + lat4 * 1.0/24.0/10; // 10=180/18
longitude -= 180.0;
latitude -= 90.0;
return true;
}
else
return false;
}
bool Maidenhead::isMaidenhead(const QString& maidenhead)
{
int length = maidenhead.length();
if ((length != 6) && (length != 8))
return false;
QRegExp re("[A-Ra-r][A-Ra-r][0-9][0-9][A-Xa-x][A-Xa-x]([0-9][0-9])?");
return re.exactMatch(maidenhead);
}

37
sdrbase/util/maidenhead.h Normal file
View File

@ -0,0 +1,37 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021 Jon Beniston, M7RCE //
// //
// Maidenhead locator utilities //
// //
// 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 <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDE_MAIDENHEAD_H
#define INCLUDE_MAIDENHEAD_H
#include <QString>
#include "export.h"
class SDRBASE_API Maidenhead {
public:
static QString toMaidenhead(float latitude, float longitude);
static bool fromMaidenhead(const QString& maidenhead, float& latitude, float& longitude);
static bool isMaidenhead(const QString& maidenhead);
};
#endif // INCLUDE_MAIDENHEAD_H