AFV initial commit

This commit is contained in:
Roland Rossgotterer
2019-09-14 21:18:26 +02:00
committed by Mat Sutcliffe
parent 7030302e73
commit b5a2f2ad13
100 changed files with 6821 additions and 25 deletions

View File

@@ -0,0 +1,42 @@
load(common_pre)
QT += dbus network multimedia gui quick
CONFIG += c++14
CONFIG -= app_bundle
CONFIG += blackmisc blackcore blackconfig
DEPENDPATH += . $$SourceRoot/src/blackmisc
INCLUDEPATH += . $$SourceRoot/src
INCLUDEPATH += $$SourceRoot/src/blackcore/afv
INCLUDEPATH += $$SourceRoot/src/blackmisc/network
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
afvmapreader.cpp \
models/atcstationmodel.cpp \
main.cpp \
HEADERS += \
models/atcstationmodel.h \
afvmapreader.h \
DEFINES += _USE_MATH_DEFINES
RESOURCES += \
qml/qml.qrc
DESTDIR = $$DestRoot/bin
load(common_post)

View File

@@ -0,0 +1,89 @@
#include "afvmapreader.h"
#include "blackcore/application.h"
#include "dto.h"
#include <QEventLoop>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
AFVMapReader::AFVMapReader(QObject *parent) : QObject(parent)
{
model = new AtcStationModel(this);
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &AFVMapReader::updateFromMap);
timer->start(3000);
}
void AFVMapReader::updateFromMap()
{
if (! sApp) { return; }
QEventLoop loop;
connect(sApp->getNetworkAccessManager(), &QNetworkAccessManager::finished, &loop, &QEventLoop::quit);
QNetworkReply *reply = sApp->getNetworkAccessManager()->get(QNetworkRequest(QUrl("https://afv-map.vatsim.net/atis-map-data")));
while (! reply->isFinished()) { loop.exec(); }
QByteArray jsonData = reply->readAll();
reply->deleteLater();
if (jsonData.isEmpty()) { return; }
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
if (jsonDoc.isObject())
{
QJsonObject rootObject = jsonDoc.object();
QVector<AtcStation> transceivers;
if (rootObject.contains("controllers"))
{
QJsonObject otherObject = rootObject.value("controllers").toObject();
for (auto it = otherObject.begin(); it != otherObject.end(); ++it)
{
QString callsign = it.key();
if (it.value().isObject())
{
QJsonObject stationObject = it.value().toObject();
if (stationObject.contains("transceivers"))
{
QJsonArray txArray = stationObject.value("transceivers").toArray();
for (auto jt = txArray.begin(); jt != txArray.end(); ++jt)
{
TransceiverDto transceiver = TransceiverDto::fromJson(jt->toObject());
transceivers.push_back( { callsign, transceiver} );
}
}
}
}
}
if (rootObject.contains("other") && rootObject.value("other").isObject())
{
QJsonObject otherObject = rootObject.value("other").toObject();
for (auto it = otherObject.begin(); it != otherObject.end(); ++it)
{
QString callsign = it.key();
if (it.value().isObject())
{
QJsonObject stationObject = it.value().toObject();
if (stationObject.contains("transceivers"))
{
QJsonArray txArray = stationObject.value("transceivers").toArray();
for (auto jt = txArray.begin(); jt != txArray.end(); ++jt)
{
TransceiverDto transceiver = TransceiverDto::fromJson(jt->toObject());
transceivers.push_back( { callsign, transceiver} );
}
}
}
}
}
if (transceivers.isEmpty()) { return; }
transceivers.erase(std::remove_if(transceivers.begin(), transceivers.end(), [this](const AtcStation &s)
{
return s.callsign() == m_callsign;
}),
transceivers.end());
model->updateAtcStations(transceivers);
}
}

View File

@@ -0,0 +1,28 @@
#ifndef AFVMAPREADER_H
#define AFVMAPREADER_H
#include "models/atcstationmodel.h"
#include <QObject>
#include <QTimer>
class AFVMapReader : public QObject
{
Q_OBJECT
Q_PROPERTY(AtcStationModel* atcStationModel READ getAtcStationModel CONSTANT)
public:
AFVMapReader(QObject *parent = nullptr);
Q_INVOKABLE void setOwnCallsign(const QString &callsign) { m_callsign = callsign; }
void updateFromMap();
AtcStationModel *getAtcStationModel() { return model; }
private:
AtcStationModel *model = nullptr;
QTimer *timer = nullptr;
QString m_callsign;
};
#endif // AFVMAPREADER_H

View File

@@ -0,0 +1,32 @@
// #include "voiceclientui.h"
#include "models/atcstationmodel.h"
#include "clients/afvclient.h"
#include "afvmapreader.h"
#include "blackcore/application.h"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QPointer>
#include <QThread>
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication qa(argc, argv);
BlackCore::CApplication a("sampleafvclient", BlackMisc::CApplicationInfo::Sample);
AFVMapReader *afvMapReader = new AFVMapReader(&a);
afvMapReader->updateFromMap();
AFVClient voiceClient("https://voice1.vatsim.uk");
QQmlApplicationEngine engine;
QQmlContext *ctxt = engine.rootContext();
ctxt->setContextProperty("afvMapReader", afvMapReader);
ctxt->setContextProperty("voiceClient", &voiceClient);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return a.exec();
}

View File

@@ -0,0 +1,122 @@
#include "atcstationmodel.h"
#include "dto.h"
#include <QtMath>
AtcStation::AtcStation(const QString &callsign, const TransceiverDto &transceiver) :
m_callsign(callsign),
m_transceiver(transceiver)
{ }
double AtcStation::latitude() const
{
return m_transceiver.LatDeg;
}
double AtcStation::longitude() const
{
return m_transceiver.LonDeg;
}
quint32 AtcStation::frequency() const
{
return m_transceiver.frequency;
}
QString AtcStation::formattedFrequency() const
{
return QString::number(m_transceiver.frequency / 1000000.0, 'f', 3);
}
double AtcStation::radioDistanceM() const
{
double sqrtAltM = qSqrt(m_transceiver.HeightMslM);
const double radioFactor = 4193.18014745372;
return radioFactor * sqrtAltM;
}
QString AtcStation::callsign() const
{
return m_callsign;
}
AtcStationModel::AtcStationModel(QObject *parent) :
QAbstractListModel(parent)
{
}
AtcStationModel::~AtcStationModel() {}
void AtcStationModel::updateAtcStations(const QVector<AtcStation> &atcStations)
{
// Add stations which didn't exist yet
for (const auto &station : atcStations)
{
if (! m_atcStations.contains(station)) { addStation(station); }
}
// Remove all stations which are no longer there
for (int i = m_atcStations.size() - 1; i >= 0; i--)
{
AtcStation &station = m_atcStations[i];
if (! m_atcStations.contains(station))
{
removeStationAtPosition(i);
}
}
}
void AtcStationModel::addStation(const AtcStation &atcStation)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_atcStations << atcStation;
endInsertRows();
}
void AtcStationModel::removeStationAtPosition(int i)
{
beginRemoveRows(QModelIndex(), i, i);
m_atcStations.removeAt(i);
endRemoveRows();
}
int AtcStationModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_atcStations.count();
}
QVariant AtcStationModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= m_atcStations.count())
return QVariant();
const AtcStation &atcStation = m_atcStations[index.row()];
if (role == CallsignRole)
return atcStation.callsign();
else if (role == LatitudeRole)
return atcStation.latitude();
else if (role == LongitudeRole)
return atcStation.longitude();
else if (role == RadioDistanceRole)
return atcStation.radioDistanceM();
else if (role == FrequencyRole)
return atcStation.formattedFrequency();
else if (role == FrequencyKhzRole)
return atcStation.frequency() / 1000;
return QVariant();
}
QHash<int, QByteArray> AtcStationModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[CallsignRole] = "callsign";
roles[LatitudeRole] = "latitude";
roles[LongitudeRole] = "longitude";
roles[RadioDistanceRole] = "radioDistanceM";
roles[FrequencyRole] = "frequencyAsString";
roles[FrequencyKhzRole] = "frequencyKhz";
return roles;
}

View File

@@ -0,0 +1,70 @@
#ifndef ATCSTATIONMODEL_H
#define ATCSTATIONMODEL_H
#include "dto.h"
#include <QtGlobal>
#include <QAbstractListModel>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
class AtcStation
{
public:
AtcStation() {}
AtcStation(const QString &callsign, const TransceiverDto &transceiver);
QString callsign() const;
double latitude() const;
double longitude() const;
quint32 frequency() const;
QString formattedFrequency() const;
double radioDistanceM() const;
private:
QString m_callsign;
TransceiverDto m_transceiver;
};
inline bool operator==(const AtcStation& lhs, const AtcStation& rhs)
{
return lhs.callsign() == rhs.callsign() &&
qFuzzyCompare(lhs.latitude(), rhs.latitude()) &&
qFuzzyCompare(lhs.longitude(), rhs.longitude());
}
class AtcStationModel : public QAbstractListModel
{
Q_OBJECT
public:
enum AtcStationRoles {
CallsignRole = Qt::UserRole + 1,
LatitudeRole,
LongitudeRole,
RadioDistanceRole,
FrequencyRole,
FrequencyKhzRole
};
AtcStationModel(QObject *parent = nullptr);
virtual ~AtcStationModel();
void updateAtcStations(const QVector<AtcStation> &atcStations);
int rowCount(const QModelIndex & parent = QModelIndex()) const override;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override;
protected:
QHash<int, QByteArray> roleNames() const override;
private:
void addStation(const AtcStation &atcStation);
void removeStationAtPosition(int i);
QList<AtcStation> m_atcStations;
};
#endif // ATCSTATIONMODEL_H

View File

@@ -0,0 +1,74 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtLocation 5.12
import QtPositioning 5.12
MapItemGroup {
id: atcRing
signal selected(real latitude, real longitude, string frequency)
property alias position: mainCircle.center
property alias radius: mainCircle.radius
property alias cs: idCallsignText.text
property alias freqAsString: idFrequency.text
property int freqKhz: 122800
MapCircle {
id: mainCircle
color: 'green'
border.width: 3
border.color: 'green'
opacity: 0.3
MouseArea {
anchors.fill: parent
onClicked: {
idCallsign.visible = !idCallsign.visible
}
onDoubleClicked: {
atcRing.selected(mainCircle.center.latitude, mainCircle.center.longitude, atcRing.freqKhz)
}
}
}
MapQuickItem {
id: circleCenter
sourceItem: Rectangle { width: 6; height: 6; color: "#000000"; border.width: 2; border.color: "#000000"; smooth: true; radius: 3 }
coordinate: mainCircle.center
opacity:1.0
anchorPoint: Qt.point(sourceItem.width/2, sourceItem.height/2)
}
MapQuickItem {
id: idCallsign
visible: false
coordinate: mainCircle.center
anchorPoint: Qt.point(-circleCenter.sourceItem.width * 0.5, circleCenter.sourceItem.height * -1.5)
sourceItem: Item {
Rectangle {
color: "#FFFFFF"
width: idCallsignText.width * 1.3
height: (idCallsignText.height + idFrequency.height) * 1.3
border.width: 2
border.color: "#000000"
radius: 5
}
Text {
id: idCallsignText
color:"#000000"
font.bold: true
}
Text {
id: idFrequency
color:"#000000"
anchors.top: idCallsignText.bottom
}
}
}
}

View File

@@ -0,0 +1,74 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
Row {
property int transceiverId: 0
property alias frequency: sbFrequency.value
spacing: 10
Label {
id: lblRadio
text: 'Radio ' + transceiverId
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
}
SpinBox {
id: sbFrequency
width: 150
height: 40
editable: true
stepSize: 25
to: 140000
from: 110000
value: 122800
property int decimals: 3
property real realValue: value / 1000
validator: DoubleValidator {
bottom: Math.min(sbFrequency.from, sbFrequency.to)
top: Math.max(sbFrequency.from, sbFrequency.to)
}
textFromValue: function(value, locale) {
return Number(value / 1000).toLocaleString(locale, 'f', sbFrequency.decimals)
}
valueFromText: function(text, locale) {
return Number.fromLocaleString(locale, text) * 1000
}
MouseArea {
anchors.fill: parent
onWheel: {
if (wheel.angleDelta.y > 0)
{
sbFrequency.value += sbFrequency.stepSize
}
else
{
sbFrequency.value -= sbFrequency.stepSize
}
wheel.accepted=true
}
}
}
CheckBox {
id: cbTxOn
height: 25
text: qsTr("TX")
checked: true
anchors.verticalCenter: parent.verticalCenter
}
CheckBox {
id: cbEnabled
height: 25
text: qsTr("Enabled")
checked: true
anchors.verticalCenter: parent.verticalCenter
}
}

View File

@@ -0,0 +1,336 @@
import QtQuick 2.12
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.12
import QtLocation 5.12
import QtPositioning 5.12
ApplicationWindow {
id: window
width: 1200
height: 520
visible: true
title: "Audio For Vatsim"
Plugin {
id: mapPlugin
name: "osm" // "mapboxgl", "esri", ...
}
Grid {
id: leftGrid
columns: 2
rows: 6
spacing: 10
padding: 10
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 5
anchors.leftMargin: 10
Label {
id: lblUsername
width: 100
text: qsTr("Username")
verticalAlignment: Text.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: false
}
TextField {
id: tfUsername
width: 350
height: 25
text: qsTr("1234567")
selectByMouse: true
horizontalAlignment: Text.AlignLeft
renderType: Text.NativeRendering
}
Label {
id: lblPassword
width: 100
text: qsTr("Password")
Layout.fillWidth: false
Layout.fillHeight: false
verticalAlignment: Text.AlignVCenter
}
TextField {
id: tfPassword
width: 350
height: 25
text: qsTr("123456")
selectByMouse: true
echoMode: TextInput.PasswordEchoOnEdit
horizontalAlignment: Text.AlignLeft
renderType: Text.NativeRendering
}
Label {
id: lblCallsign
width: 100
text: qsTr("Callsign")
Layout.fillWidth: false
Layout.fillHeight: false
verticalAlignment: Text.AlignVCenter
}
TextField {
id: tfCallsign
width: 350
height: 25
text: qsTr("DECHK")
selectByMouse: true
horizontalAlignment: Text.AlignLeft
renderType: Text.NativeRendering
}
Label {
id: lblInputDevice
width: 100
text: qsTr("Input Device")
verticalAlignment: Text.AlignVCenter
Layout.fillHeight: false
Layout.fillWidth: false
}
ComboBox {
id: cbInputDevices
width: 350
height: 25
model: voiceClient.availableInputDevices()
}
Label {
id: lblOutputDevice
width: 100
text: qsTr("Output Device")
verticalAlignment: Text.AlignVCenter
Layout.fillHeight: false
Layout.fillWidth: false
}
ComboBox {
id: cbOutputDevices
width: 350
height: 25
model: voiceClient.availableOutputDevices()
}
Frame {
background: Rectangle {
color: "transparent"
border.color: "transparent"
}
}
Row {
spacing: 10
Button {
id: btConnect
property bool connected: false
width: 170
height: 25
text: qsTr("Connect")
onClicked: {
if (btConnect.connected) {
btConnect.connected = false;
btConnect.text = qsTr("Connect")
voiceClient.disconnectFrom()
} else {
btConnect.connected = true
btConnect.text = qsTr("Disconnect")
voiceClient.connectTo(tfUsername.text, tfPassword.text, tfCallsign.text)
afvMapReader.setOwnCallsign(tfCallsign.text)
}
}
}
Button {
id: btStartAudio
property bool started: false
width: 170
height: 25
text: qsTr("Start Audio")
onClicked: {
btStartAudio.enabled = false
cbInputDevices.enabled = false
cbOutputDevices.enabled = false
voiceClient.start(cbInputDevices.currentText, cbOutputDevices.currentText)
}
}
}
}
Grid {
id: rightGrid
padding: 10
anchors.top: parent.top
anchors.left: leftGrid.right
anchors.right: parent.right
spacing: 10
rows: 2
columns: 3
Transceiver { id: transceiver1; transceiverId: 0 }
SpinBox {
id: sbAltitude
width: 150
height: 40
stepSize: 500
to: 50000
from: 0
value: 1000
}
Label {
id: lblReceivingCom1
height: 40
text: qsTr("Receiving:")
verticalAlignment: Text.AlignVCenter
}
Transceiver { id: transceiver2; transceiverId: 1 }
Button {
id: btUpdateStack
width: 150
height: 40
text: qsTr("Update Stack")
onClicked: {
voiceClient.updateComFrequency(0, transceiver1.frequency * 1000)
voiceClient.updateComFrequency(1, transceiver2.frequency * 1000)
voiceClient.updatePosition(map.center.latitude, map.center.longitude, sbAltitude.value)
voiceClient.updateTransceivers()
}
}
Label {
id: lblReceivingCom2
height: 40
text: qsTr("Receiving:")
verticalAlignment: Text.AlignVCenter
// anchors.verticalCenter: parent.verticalCenter
}
}
Column {
id: column
padding: 10
spacing: 10
anchors.top: rightGrid.bottom
anchors.left: leftGrid.right
anchors.right: parent.right
ProgressBar {
id: pbAudioInput
width: 500
height: 25
anchors.left: parent.left
anchors.leftMargin: 10
value: voiceClient.inputVolumePeakVU
}
ProgressBar {
id: pbAudioOutput
width: 500
height: 25
anchors.left: parent.left
anchors.leftMargin: 10
value: voiceClient.outputVolumePeakVU
}
}
//// CheckBox {
//// id: cbVhfEffects
//// anchors.topMargin: 5
//// anchors.leftMargin: 10
//// anchors.left: sbAltitude.right
//// anchors.top: parent.top
//// anchors.verticalCenter: sbAltitude.verticalCenter
//// height: 25
//// text: qsTr("VHF Effects")
//// checked: true
//// }
Map {
id: map
anchors.topMargin: 5
anchors.top: leftGrid.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
plugin: mapPlugin
center: QtPositioning.coordinate(48.50, 11.50) // Oslo
zoomLevel: 3
// MapCircle {
// center {
// latitude: map.center.latitude
// longitude: map.center.longitude
// }
// radius: 500000.0
// color: 'blue'
// border.width: 3
// border.color: 'blue'
// opacity: 0.3
// }
MapItemView {
model: afvMapReader.atcStationModel
delegate: atcDelegate
}
Component {
id: atcDelegate
AtcRing {
position {
latitude: latitude
longitude: longitude
}
radius: radioDistanceM
cs: callsign
freqAsString: frequencyAsString
freqKhz: frequencyKhz
onSelected: {
map.center = QtPositioning.coordinate(latitude, longitude)
transceiver1.frequency = frequency
voiceClient.updateComFrequency(0, transceiver1.frequency * 1000)
voiceClient.updateComFrequency(1, transceiver2.frequency * 1000)
voiceClient.updatePosition(map.center.latitude, map.center.longitude, sbAltitude.value)
}
}
}
Rectangle {
width: 3
height: 15
color: "blue"
anchors.verticalCenter: map.verticalCenter
anchors.horizontalCenter: map.horizontalCenter
}
Rectangle {
width: 15
height: 3
color: "blue"
anchors.verticalCenter: map.verticalCenter
anchors.horizontalCenter: map.horizontalCenter
}
}
Timer {
interval: 5000; running: true; repeat: true
onTriggered: {
voiceClient.updateComFrequency(0, transceiver1.frequency * 1000)
voiceClient.updateComFrequency(1, transceiver2.frequency * 1000)
voiceClient.updatePosition(map.center.latitude, map.center.longitude, sbAltitude.value)
}
}
}

View File

@@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/">
<file>main.qml</file>
<file>AtcRing.qml</file>
<file>Transceiver.qml</file>
</qresource>
</RCC>