diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt
index b9a1b553d..bb427681a 100644
--- a/plugins/feature/CMakeLists.txt
+++ b/plugins/feature/CMakeLists.txt
@@ -5,6 +5,7 @@ if (Qt5SerialPort_FOUND)
endif()
if (Qt5Quick_FOUND AND Qt5QuickWidgets_FOUND AND Qt5Positioning_FOUND)
+ add_subdirectory(map)
add_subdirectory(vorlocalizer)
endif()
diff --git a/plugins/feature/map/CMakeLists.txt b/plugins/feature/map/CMakeLists.txt
new file mode 100644
index 000000000..b5c87a2a2
--- /dev/null
+++ b/plugins/feature/map/CMakeLists.txt
@@ -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})
diff --git a/plugins/feature/map/map.cpp b/plugins/feature/map/map.cpp
new file mode 100644
index 000000000..0b71366e2
--- /dev/null
+++ b/plugins/feature/map/map.cpp
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+
+#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 availablePipes = updateAvailablePipeSources("mapitems", MapSettings::m_pipeTypes, MapSettings::m_pipeURIs, this);
+
+ if (availablePipes != m_availablePipes)
+ {
+ m_availablePipes = availablePipes;
+ if (getMessageQueueToGUI())
+ {
+ MsgReportPipes *msgToGUI = MsgReportPipes::create();
+ QList& 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 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& 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();
+}
diff --git a/plugins/feature/map/map.h b/plugins/feature/map/map.h
new file mode 100644
index 000000000..0379cef7c
--- /dev/null
+++ b/plugins/feature/map/map.h
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDE_FEATURE_MAP_H_
+#define INCLUDE_FEATURE_MAP_H_
+
+#include
+#include
+#include
+#include
+
+#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 m_availablePipes;
+ QTimer m_updatePipesTimer;
+
+ QNetworkAccessManager *m_networkManager;
+ QNetworkRequest m_networkRequest;
+
+ void applySettings(const MapSettings& settings, bool force = false);
+ void webapiReverseSendSettings(QList& featureSettingsKeys, const MapSettings& settings, bool force);
+
+private slots:
+ void updatePipes();
+ void networkManagerFinished(QNetworkReply *reply);
+};
+
+#endif // INCLUDE_FEATURE_MAP_H_
diff --git a/plugins/feature/map/map.qrc b/plugins/feature/map/map.qrc
new file mode 100644
index 000000000..4299b3178
--- /dev/null
+++ b/plugins/feature/map/map.qrc
@@ -0,0 +1,7 @@
+
+
+ map/map.qml
+ map/MapStation.qml
+ map/antenna.png
+
+
diff --git a/plugins/feature/map/map/MapStation.qml b/plugins/feature/map/map/MapStation.qml
new file mode 100644
index 000000000..a69346e46
--- /dev/null
+++ b/plugins/feature/map/map/MapStation.qml
@@ -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
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/feature/map/map/antenna.png b/plugins/feature/map/map/antenna.png
new file mode 100644
index 000000000..f13c91881
Binary files /dev/null and b/plugins/feature/map/map/antenna.png differ
diff --git a/plugins/feature/map/map/map.qml b/plugins/feature/map/map/map.qml
new file mode 100644
index 000000000..54d78d096
--- /dev/null
+++ b/plugins/feature/map/map/map.qml
@@ -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
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp
new file mode 100644
index 000000000..bd854e44f
--- /dev/null
+++ b/plugins/feature/map/mapgui.cpp
@@ -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 . //
+///////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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