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(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("map"); + if(object != NULL) + { + QGeoCoordinate coords = object->property("center").value(); + 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("station"); + if(stationObject != NULL) + { + QGeoCoordinate coords = stationObject->property("coordinate").value(); + 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::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("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(); +} diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h new file mode 100644 index 000000000..c78c8e88b --- /dev/null +++ b/plugins/feature/map/mapgui.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MAPGUI_H_ +#define INCLUDE_FEATURE_MAPGUI_H_ + +#include +#include +#include + +#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 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 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 roleNames() const + { + QHash 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 m_items; + QList 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 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_ diff --git a/plugins/feature/map/mapgui.ui b/plugins/feature/map/mapgui.ui new file mode 100644 index 000000000..95ba4709f --- /dev/null +++ b/plugins/feature/map/mapgui.ui @@ -0,0 +1,250 @@ + + + MapGUI + + + + 0 + 0 + 462 + 689 + + + + + 0 + 0 + + + + + 462 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + Map + + + Map + + + + + 0 + 0 + 461 + 41 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + + Sources + + + + + + + + 150 + 0 + + + + Data source channels and features + + + + + + + Find + + + + + + + Enter name of object to find, latitude and longitude or Maidenhead locator + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Display names + + + ^ + + + + :/info.png:/info.png + + + true + + + true + + + + + + + Delete all items on the map + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 0 + 100 + 461 + 581 + + + + + 0 + 0 + + + + Map + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 0 + 0 + + + + + 100 + 500 + + + + Map + + + QQuickWidget::SizeRootObjectToView + + + + + + + + + + + + + + QQuickWidget + QWidget +
QtQuickWidgets/QQuickWidget
+
+ + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+
+ + map + + + + + +
diff --git a/plugins/feature/map/mapplugin.cpp b/plugins/feature/map/mapplugin.cpp new file mode 100644 index 000000000..774be51e1 --- /dev/null +++ b/plugins/feature/map/mapplugin.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + + +#include +#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(); +} diff --git a/plugins/feature/map/mapplugin.h b/plugins/feature/map/mapplugin.h new file mode 100644 index 000000000..47ffe16f6 --- /dev/null +++ b/plugins/feature/map/mapplugin.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MAPPLUGIN_H +#define INCLUDE_FEATURE_MAPPLUGIN_H + +#include +#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 diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp new file mode 100644 index 000000000..5f88c43ea --- /dev/null +++ b/plugins/feature/map/mapsettings.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#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; + } +} diff --git a/plugins/feature/map/mapsettings.h b/plugins/feature/map/mapsettings.h new file mode 100644 index 000000000..2550b81ef --- /dev/null +++ b/plugins/feature/map/mapsettings.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_MAPSETTINGS_H_ +#define INCLUDE_FEATURE_MAPSETTINGS_H_ + +#include +#include + +#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_ diff --git a/plugins/feature/map/mapwebapiadapter.cpp b/plugins/feature/map/mapwebapiadapter.cpp new file mode 100644 index 000000000..0ef0500b2 --- /dev/null +++ b/plugins/feature/map/mapwebapiadapter.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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; +} diff --git a/plugins/feature/map/mapwebapiadapter.h b/plugins/feature/map/mapwebapiadapter.h new file mode 100644 index 000000000..db29b7036 --- /dev/null +++ b/plugins/feature/map/mapwebapiadapter.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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 diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md new file mode 100644 index 000000000..2dbe55efa --- /dev/null +++ b/plugins/feature/map/readme.md @@ -0,0 +1,43 @@ +

Map Feature Plugin

+ +

Introduction

+ +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. + +

Interface

+ +![Map feature plugin GUI](../../../doc/img/Map_plugin.png) + +

1: Source Channels

+ +This displays the list of channels the Map is displaying data from. + +

2: Find

+ +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). + +

3: Display Names

+ +When checked, names of objects are displayed in a bubble next to each object. + +

Map

+ +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. + +

API

+ +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" }}' diff --git a/sdrbase/feature/featurewebapiutils.cpp b/sdrbase/feature/featurewebapiutils.cpp new file mode 100644 index 000000000..33c07ee44 --- /dev/null +++ b/sdrbase/feature/featurewebapiutils.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#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& 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::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; + } +} diff --git a/sdrbase/feature/featurewebapiutils.h b/sdrbase/feature/featurewebapiutils.h new file mode 100644 index 000000000..808ac6665 --- /dev/null +++ b/sdrbase/feature/featurewebapiutils.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#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_ diff --git a/sdrbase/util/maidenhead.cpp b/sdrbase/util/maidenhead.cpp new file mode 100644 index 000000000..d7c6eab5d --- /dev/null +++ b/sdrbase/util/maidenhead.cpp @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#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); +} diff --git a/sdrbase/util/maidenhead.h b/sdrbase/util/maidenhead.h new file mode 100644 index 000000000..0147e0f7b --- /dev/null +++ b/sdrbase/util/maidenhead.h @@ -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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_MAIDENHEAD_H +#define INCLUDE_MAIDENHEAD_H + +#include + +#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