From 9f351bae2f784623254dabd758af25699bf3f77b Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 26 Jan 2026 21:03:47 +0100 Subject: [PATCH 01/30] WIP Add network position provider This commit adds both UDP & TCP sockets under one network provider. We further differ external provider into bluetooth and network providers. --- app/CMakeLists.txt | 2 + app/appsettings.cpp | 8 +- app/position/positionkit.cpp | 74 ++++---- .../providers/bluetoothpositionprovider.cpp | 4 +- .../providers/networkpositionprovider.cpp | 168 ++++++++++++++++++ .../providers/networkpositionprovider.h | 64 +++++++ .../providers/positionprovidersmodel.cpp | 11 +- .../providers/positionprovidersmodel.h | 2 +- app/qml/gps/MMGpsDataDrawer.qml | 11 +- app/qml/gps/MMPositionProviderPage.qml | 97 +++++++++- app/qml/map/MMMapController.qml | 4 +- app/test/testposition.cpp | 10 +- app/test/testvariablesmanager.cpp | 2 +- gallery/positionkit.h | 2 +- 14 files changed, 398 insertions(+), 61 deletions(-) create mode 100644 app/position/providers/networkpositionprovider.cpp create mode 100644 app/position/providers/networkpositionprovider.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index e474aa9b2..ed7a78f83 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -32,6 +32,7 @@ set(MM_SRCS ios/iosutils.cpp position/providers/abstractpositionprovider.cpp position/providers/internalpositionprovider.cpp + position/providers/networkpositionprovider.cpp position/providers/positionprovidersmodel.cpp position/providers/simulatedpositionprovider.cpp position/tracking/abstracttrackingbackend.cpp @@ -122,6 +123,7 @@ set(MM_HDRS ios/iosutils.h position/providers/abstractpositionprovider.h position/providers/internalpositionprovider.h + position/providers/networkpositionprovider.h position/providers/positionprovidersmodel.h position/providers/simulatedpositionprovider.h position/tracking/abstracttrackingbackend.h diff --git a/app/appsettings.cpp b/app/appsettings.cpp index 58e340ec1..e781e108a 100644 --- a/app/appsettings.cpp +++ b/app/appsettings.cpp @@ -215,6 +215,7 @@ QVariantList AppSettings::savedPositionProviders() const QStringList provider; provider << settings.value( "providerName" ).toString(); provider << settings.value( "providerAddress" ).toString(); + provider << settings.value( "providerType" ).toString(); providers.push_back( provider ); } @@ -238,15 +239,16 @@ void AppSettings::savePositionProviders( const QVariantList &providers ) { QVariantList provider = providers[i].toList(); - if ( provider.length() < 2 ) + if ( provider.length() < 3 ) { CoreUtils::log( QStringLiteral( "AppSettings" ), QStringLiteral( "Tried to save provider without sufficient data" ) ); continue; } settings.setArrayIndex( i ); - settings.setValue( "providerName", providers[i].toList()[0] ); - settings.setValue( "providerAddress", providers[i].toList()[1] ); + settings.setValue( "providerName", provider[0] ); + settings.setValue( "providerAddress", provider[1] ); + settings.setValue( "providerType", provider[2] ); } settings.endArray(); } diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 85c6677a5..1e0be92ba 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -18,6 +18,7 @@ #include "position/providers/internalpositionprovider.h" #include "position/providers/simulatedpositionprovider.h" +#include "providers/networkpositionprovider.h" #ifdef ANDROID #include "position/providers/androidpositionprovider.h" #include @@ -128,13 +129,12 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c { QString providerType( type ); - // currently the only external provider is bluetooth, so manually set internal provider for platforms that - // do not support reading bluetooth serial + // set internal provider for platforms that do not support reading bluetooth serial #ifndef HAVE_BLUETOOTH providerType = QStringLiteral( "internal" ); #endif - if ( providerType == QStringLiteral( "external" ) ) + if ( providerType == QStringLiteral( "external_bt" ) ) { #ifdef HAVE_BLUETOOTH AbstractPositionProvider *provider = new BluetoothPositionProvider( id, name, *mPositionTransformer ); @@ -142,39 +142,47 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c return provider; #endif } - else // type == internal + + if ( providerType == QStringLiteral( "external_ip" ) ) { - if ( id == QStringLiteral( "simulated" ) ) - { - AbstractPositionProvider *provider = new SimulatedPositionProvider( *mPositionTransformer ); - QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); - return provider; - } + AbstractPositionProvider *provider = new NetworkPositionProvider( id, name ); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } + + // type == internal + if ( id == QStringLiteral( "simulated" ) ) + { + AbstractPositionProvider *provider = new SimulatedPositionProvider(); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } + #ifdef ANDROID - if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) + if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) + { + bool fused = ( id == QStringLiteral( "android_fused" ) ); + if ( fused && !AndroidPositionProvider::isFusedAvailable() ) { - const bool fused = ( id == QStringLiteral( "android_fused" ) ); - if ( fused && !AndroidPositionProvider::isFusedAvailable() ) - { - // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? + // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? - // fallback to the default - at this point the Qt Positioning implementation - AbstractPositionProvider *provider = new InternalPositionProvider( *mPositionTransformer ); - QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); - return provider; - } - __android_log_print( ANDROID_LOG_INFO, "CPP", "MAKE PROVIDER %d", fused ); - AbstractPositionProvider *provider = new AndroidPositionProvider( fused, *mPositionTransformer ); + // fallback to the default - at this point the Qt Positioning implementation + AbstractPositionProvider *provider = new InternalPositionProvider(); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } + __android_log_print( ANDROID_LOG_INFO, "CPP", "MAKE PROVIDER %d", fused ); + AbstractPositionProvider *provider = new AndroidPositionProvider( fused ); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } #endif - else // id == devicegps - { - AbstractPositionProvider *provider = new InternalPositionProvider( *mPositionTransformer ); - QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); - return provider; - } + + // id == devicegps + { + AbstractPositionProvider *provider = new InternalPositionProvider(); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; } } @@ -207,8 +215,10 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app } else { - // find name of the active provider + // find name & type of the active provider QString providerName; + // Migration from single external provider to multiple currently, missing type == bluetooth provider + QString providerType = QStringLiteral( "external_bt" ); QVariantList providers = appsettings->savedPositionProviders(); for ( const auto &provider : providers ) @@ -223,10 +233,14 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app if ( providerData[1] == providerId ) { providerName = providerData[0].toString(); + if ( !providerData.at( 3 ).isNull() ) + { + providerType = providerData[3].toString(); + } } } - return constructProvider( QStringLiteral( "external" ), providerId, providerName ); + return constructProvider( providerType, providerId, providerName ); } } diff --git a/app/position/providers/bluetoothpositionprovider.cpp b/app/position/providers/bluetoothpositionprovider.cpp index c96a235db..6303bb37d 100644 --- a/app/position/providers/bluetoothpositionprovider.cpp +++ b/app/position/providers/bluetoothpositionprovider.cpp @@ -30,8 +30,8 @@ QgsGpsInformation NmeaParser::parseNmeaString( const QString &nmeaString ) return mLastGPSInformation; } -BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external" ), name, positionTransformer, parent ) +BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, parent ) , mTargetAddress( addr ) { mSocket = std::unique_ptr( new QBluetoothSocket( QBluetoothServiceInfo::RfcommProtocol ) ); diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp new file mode 100644 index 000000000..d96b3bcc6 --- /dev/null +++ b/app/position/providers/networkpositionprovider.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** +* * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "networkpositionprovider.h" + +#include "coreutils.h" + +static int ONE_SECOND_MS = 1000; + +NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ) +{ + const QStringList targetAddress = addr.split( ":" ); + mTargetAddress = targetAddress.at( 0 ); + mTargetPort = targetAddress.at( 1 ).toInt(); + + mTcpSocket = std::make_unique(); + mUdpSocket = std::make_unique(); + +// only one socket will be active depending on, which will find host, the other will be closed +// connect(mTcpSocket.get(), &QAbstractSocket::hostFound, this, [this] +// { +// CoreUtils::log("NetworkPositionProvider", "TCP socket found host, aborting UDP socket"); +// mUdpSocket->abort(); +// }); +// connect(mUdpSocket.get(), &QAbstractSocket::hostFound, this, [this] +// { +// CoreUtils::log("NetworkPositionProvider", "UDP socket found host, aborting TCP socket"); +// mTcpSocket->abort(); +// }); + + connect( mTcpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + connect( mUdpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + + connect( mTcpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + connect( mUdpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + + mReconnectTimer.setSingleShot( false ); + mReconnectTimer.setInterval( ONE_SECOND_MS ); + connect( &mReconnectTimer, &QTimer::timeout, this, &NetworkPositionProvider::reconnectTimeout ); + + NetworkPositionProvider::startUpdates(); +} + +void NetworkPositionProvider::startUpdates() +{ + CoreUtils::log( "NetworkPositionProvider", "Connecting to host..." ); + mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); + mUdpSocket->connectToHost( mTargetAddress, mTargetPort ); +} + +void NetworkPositionProvider::stopUpdates() +{ + if ( mTcpSocket->state() != QAbstractSocket::UnconnectedState && mTcpSocket->state() != QAbstractSocket::ClosingState ) + { + mTcpSocket->disconnectFromHost(); + } + if ( mUdpSocket->state() != QAbstractSocket::UnconnectedState && mUdpSocket->state() != QAbstractSocket::ClosingState ) + { + mUdpSocket->disconnectFromHost(); + } +} + +void NetworkPositionProvider::closeProvider() +{ + mTcpSocket->close(); + mUdpSocket->close(); +} + +void NetworkPositionProvider::positionUpdateReceived() +{ + const QAbstractSocket *socket = dynamic_cast( sender() ); + const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); + CoreUtils::log( "NetworkPositionProvider", QStringLiteral( "%1 socket has received new data!" ).arg( socketTypeToString ) ); + if ( mTcpSocket->isValid() || mUdpSocket->isValid() ) + { + const QByteArray rawNmeaData = activeSocket()->readAll(); + const QString nmeaData( rawNmeaData ); + const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + + emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + } +} + +void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketState state ) +{ + if ( state == QAbstractSocket::SocketState::ConnectingState || state == QAbstractSocket::SocketState::HostLookupState ) + { + setState( tr( "Connecting to %1" ).arg( mProviderName ), State::Connecting ); + } + else if ( state == QAbstractSocket::SocketState::ConnectedState ) + { + setState( tr( "Connected" ), State::Connected ); + } + else if ( state == QAbstractSocket::SocketState::UnconnectedState ) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + const QAbstractSocket *socket = dynamic_cast( sender() ); + const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); + const QString stateToString = QMetaEnum::fromType().valueToKey( state ); + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "%1 Socket changed state, code: %2" ).arg( socketTypeToString, stateToString ) ); +} + +void NetworkPositionProvider::reconnectTimeout() +{ + if ( mSecondsLeftToReconnect <= 1 ) + { + reconnect(); + } + else + { + mSecondsLeftToReconnect--; + setState( tr( "No connection, reconnecting in (%1)" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + mReconnectTimer.start(); + } +} + +QAbstractSocket *NetworkPositionProvider::activeSocket() const +{ + if ( mTcpSocket->state() >= QAbstractSocket::ConnectingState && mTcpSocket->state() <= QAbstractSocket::BoundState ) + { + return mTcpSocket.get(); + } + + if ( mUdpSocket->state() >= QAbstractSocket::ConnectingState && mUdpSocket->state() <= QAbstractSocket::BoundState ) + { + return mUdpSocket.get(); + } + +//TODO: wouldn't it be better to guard against nullptr in caller and return nullptr here? + return new QAbstractSocket( QAbstractSocket::UnknownSocketType, nullptr ); +} + +void NetworkPositionProvider::reconnect() +{ + mReconnectTimer.stop(); + + setState( tr( "Reconnecting" ), State::Connecting ); + + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "Reconnecting to %1" ).arg( mProviderName ) ); + + stopUpdates(); + startUpdates(); +} + +void NetworkPositionProvider::startReconnectTimer() +{ + mSecondsLeftToReconnect = mReconnectDelay / ONE_SECOND_MS; + setState( tr( "No connection, reconnecting in (%1)" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + + mReconnectTimer.start(); + +// first time do reconnect in short time, then each other in long time + if ( mReconnectDelay == NetworkPositionProvider::ShortDelay ) + { + mReconnectDelay = NetworkPositionProvider::LongDelay; + } +} diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h new file mode 100644 index 000000000..5a8755417 --- /dev/null +++ b/app/position/providers/networkpositionprovider.h @@ -0,0 +1,64 @@ +/*************************************************************************** +* * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + + +#ifndef NETWORKPOSITIONPROVIDER_H +#define NETWORKPOSITIONPROVIDER_H + +#include +#include + +#include "abstractpositionprovider.h" +#include "bluetoothpositionprovider.h" + + +class NetworkPositionProvider : public AbstractPositionProvider +{ + Q_OBJECT + +// signalizes in how many [ms] we will try to reconnect to GPS again + enum ReconnectDelay + { + ShortDelay = 3000, + LongDelay = 5000 + }; + + public: + explicit NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent = nullptr ); + + void startUpdates() override; + void stopUpdates() override; + void closeProvider() override; + + public slots: + void positionUpdateReceived(); + void socketStateChanged( QAbstractSocket::SocketState state ); + void reconnectTimeout(); + + private: +// utility function, which should return the socket in use + QAbstractSocket *activeSocket() const; +// trigger the reconnection flow for both sockets + void reconnect(); + // start the reconnection timeout + void startReconnectTimer(); + + std::unique_ptr mTcpSocket; + std::unique_ptr mUdpSocket; + + int mReconnectDelay = ReconnectDelay::ShortDelay; // in how many [ms] we will try to reconnect again + int mSecondsLeftToReconnect; // how many seconds are left to reconnect. Reconnects if less than or equal to one + QTimer mReconnectTimer; // timer that times out each second and lowers the mSecondsLeftToReconnect by one + + QString mTargetAddress; // IP address or hostname of the receiver + int mTargetPort; // active port of the receiver + + NmeaParser mNmeaParser; // parser to decode received NMEA strings +}; +#endif //NETWORKPOSITIONPROVIDER_H diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index e070e4576..b51373f2b 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -134,16 +134,17 @@ void PositionProvidersModel::removeProvider( const QString &providerId ) } } -void PositionProvidersModel::addProvider( const QString &name, const QString &providerId ) +void PositionProvidersModel::addProvider( const QString &name, const QString &providerId, const QString &providerType ) { if ( providerId.isEmpty() ) return; PositionProvider toAdd; + const QString deviceDesc = providerType == QStringLiteral( "external_bt" ) ? tr( " Bluetooth device" ) : tr( " Network device" ); toAdd.name = name; toAdd.providerId = providerId; - toAdd.description = providerId + " " + tr( " Bluetooth device" ); - toAdd.providerType = "external"; + toAdd.description = providerId + " " + deviceDesc; + toAdd.providerType = providerType; if ( mProviders.contains( toAdd ) ) return; @@ -218,8 +219,8 @@ QVariantList PositionProvidersModel::toVariantList() const if ( mProviders[i].providerType == QStringLiteral( "internal" ) ) continue; - QStringList a = { mProviders[i].name, mProviders[i].providerId }; - out.push_back( a ); + QStringList provider = { mProviders[i].name, mProviders[i].providerId, mProviders[i].providerType }; + out.push_back( provider ); } return out; diff --git a/app/position/providers/positionprovidersmodel.h b/app/position/providers/positionprovidersmodel.h index dc24171fe..a05589504 100644 --- a/app/position/providers/positionprovidersmodel.h +++ b/app/position/providers/positionprovidersmodel.h @@ -67,7 +67,7 @@ class PositionProvidersModel : public QAbstractListModel QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; Q_INVOKABLE void removeProvider( const QString &providerId ); - Q_INVOKABLE void addProvider( const QString &providerName, const QString &providerId ); + Q_INVOKABLE void addProvider( const QString &providerName, const QString &providerId, const QString &providerType ); AppSettings *appSettings() const; void setAppSettings( AppSettings * ); diff --git a/app/qml/gps/MMGpsDataDrawer.qml b/app/qml/gps/MMGpsDataDrawer.qml index 1b018bbe3..ad4456e8c 100644 --- a/app/qml/gps/MMGpsDataDrawer.qml +++ b/app/qml/gps/MMGpsDataDrawer.qml @@ -73,7 +73,7 @@ MMComponents.MMDrawer { width: parent.width / 2 - visible: PositionKit.positionProvider && PositionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") title: qsTr( "Status" ) value: PositionKit.positionProvider ? PositionKit.positionProvider.stateMessage : "" @@ -214,7 +214,7 @@ MMComponents.MMDrawer { PositionKit.fix } - visible: PositionKit.positionProvider && PositionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -247,7 +247,7 @@ MMComponents.MMDrawer { __inputUtils.formatNumber( PositionKit.hdop, 2 ) } - visible: PositionKit.positionProvider && PositionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -264,7 +264,7 @@ MMComponents.MMDrawer { __inputUtils.formatNumber( PositionKit.vdop, 2 ) } - visible: PositionKit.positionProvider && PositionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -281,7 +281,7 @@ MMComponents.MMDrawer { __inputUtils.formatNumber( PositionKit.pdop, 2 ) } - visible: PositionKit.positionProvider && PositionKit.positionProvider.type() === "external" + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } @@ -329,6 +329,7 @@ MMComponents.MMDrawer { } __inputUtils.formatNumber( PositionKit.geoidSeparation, 2 ) + " m" } + visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") alignmentRight: Positioner.index % 2 === 1 } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index c98d9a0c8..5ff5e5964 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -8,6 +8,7 @@ ***************************************************************************/ import QtQuick +import QtQml.Models import QtQuick.Controls import QtQuick.Dialogs @@ -16,6 +17,7 @@ import MMInput import "../components" as MMComponents import "../dialogs" as MMDialogs +import "../inputs" as MMInputs MMComponents.MMPage { id: root @@ -94,7 +96,7 @@ MMComponents.MMPage { if ( ListView.section === "internal" ) { let ix = providersModel.index( index + 1, 0 ) let type = providersModel.data( ix, MM.PositionProvidersModel.ProviderType ) - if ( type === "external" ) return false + if ( type.includes("external") ) return false } return true @@ -135,7 +137,90 @@ MMComponents.MMPage { text: qsTr( "Connect new receiver" ) - onClicked: bluetoothDiscoveryLoader.active = true + onClicked: providerTypeDrawer.open() + } + + MMComponents.MMListDrawer { + id: providerTypeDrawer + + drawerHeader.title: qsTr("Pick the correct provider type") + drawerHeader.titleFont: __style.t2 + + list.model: ListModel { + ListElement { + name: qsTr( "Bluetooth provider" ) + type: "bluetooth" + } + ListElement { + name: qsTr( "Network provider" ) + type: "network" + } + } + + list.delegate: MMComponents.MMListDelegate { + required property string name + required property string type + + text: name + onClicked: { + providerTypeDrawer.close() + if (type === "bluetooth") { + bluetoothDiscoveryLoader.active = true + } + else if (type === "network"){ + networkProviderInfoDrawer.open() + } + } + } + } + + MMComponents.MMDrawerDialog { + id: networkProviderInfoDrawer + + signal confirmButtonClicked(string address, int port) + + title: qsTr("Network provider setup") + imageSource: __style.externalGpsGreenImage + description: qsTr( "To connect to the external device please specify the IP address and port below." ) + primaryButton.text: qsTr( "Confirm" ) + primaryButton.type: MMComponents.MMButton.Primary + + additionalContent: Column { + width: parent.width + spacing: __style.spacing20 + + MMInputs.MMTextInput { + id: ipAddressInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr("IP address") + placeholderText: qsTr("localhost") + } + + MMInputs.MMTextInput { + id: portInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr("Port") + placeholderText: qsTr("1234") + } + } + + onPrimaryButtonClicked: { + close() + confirmButtonClicked(ipAddressInput.text, portInput.text) + } + + onConfirmButtonClicked: function( address, port ) { + const deviceAddress = address + ":" + port + __positionKit.positionProvider = __positionKit.constructProvider( "external_ip", deviceAddress, "Network provider" ) + + providersModel.addProvider( "Network provider", deviceAddress, "external_ip" ) + } } MMComponents.MMMessage { @@ -205,9 +290,9 @@ MMComponents.MMPage { MMAddPositionProviderDrawer { onInitiatedConnectionTo: function ( deviceAddress, deviceName ) { - PositionKit.positionProvider = PositionKit.constructProvider( "external", deviceAddress, deviceName ) + __positionKit.positionProvider = __positionKit.constructProvider( "external_bt", deviceAddress, deviceName ) - providersModel.addProvider( deviceName, deviceAddress ) + providersModel.addProvider( deviceName, deviceAddress, "external_bt" ) list.model.discovering = false close() @@ -239,7 +324,7 @@ MMComponents.MMPage { } function constructProvider( type, id, name ) { - if ( type === "external" ) { + if ( type === "external_bt" ) { // Is bluetooth turned on? if ( !__inputUtils.isBluetoothTurnedOn() ) { __inputUtils.turnBluetoothOn() @@ -253,7 +338,7 @@ MMComponents.MMPage { PositionKit.positionProvider = PositionKit.constructProvider( type, id, name ) - if ( type === "external" ) { + if ( type === "external_bt" ) { connectingDialogLoader.open() } } diff --git a/app/qml/map/MMMapController.qml b/app/qml/map/MMMapController.qml index ae892281d..6766d3f32 100644 --- a/app/qml/map/MMMapController.qml +++ b/app/qml/map/MMMapController.qml @@ -679,7 +679,7 @@ Item { visible: { if ( root.mapExtentOffset > 0 && root.state !== "stakeout" ) return false - if ( PositionKit.positionProvider && PositionKit.positionProvider.type() === "external" ) { + if ( __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") ) { // for external receivers we want to show gps panel and accuracy button // even when the GPS receiver is not sending position data return true @@ -699,7 +699,7 @@ Item { { return "" } - else if ( PositionKit.positionProvider.type() === "external" ) + else if ( __positionKit.positionProvider.type().includes("external") ) { if ( PositionKit.positionProvider.state === MM.PositionProvider.Connecting ) { diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 6c0bdba26..740ef6f43 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -106,8 +106,8 @@ void TestPosition::testBluetoothProviderConnection() QCOMPARE( "testBluetoothProvider", pkProvider->name() ); QCOMPARE( "AA:AA:AA:AA:00:00", btProvider->id() ); QCOMPARE( "AA:AA:AA:AA:00:00", pkProvider->id() ); - QCOMPARE( "external", btProvider->type() ); - QCOMPARE( "external", pkProvider->type() ); + QCOMPARE( "external_bt", btProvider->type() ); + QCOMPARE( "external_bt", pkProvider->type() ); // // let's continue with BT instance, @@ -257,11 +257,11 @@ void TestPosition::testPositionProviderKeysInSettings() rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests #ifdef HAVE_BLUETOOTH - positionKit->setPositionProvider( positionKit->constructProvider( "external", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); + positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); QCOMPARE( positionKit->positionProvider()->name(), "testProviderA" ); - QCOMPARE( positionKit->positionProvider()->type(), "external" ); + QCOMPARE( positionKit->positionProvider()->type(), "external_bt" ); QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "AA:BB:CC:DD:EE:FF" ); #endif @@ -279,7 +279,7 @@ void TestPosition::testPositionProviderKeysInSettings() QCOMPARE( providersModel.data( providersModel.index( 1 ), PositionProvidersModel::ProviderId ), "simulated" ); providersModel.setAppSettings( &appSettings ); - providersModel.addProvider( "testProviderB", "AA:00:11:22:23:44" ); + providersModel.addProvider( "testProviderB", "AA:00:11:22:23:44", "external_bt" ); // app settings should have one saved provider - testProviderB QVariantList providers = appSettings.savedPositionProviders(); diff --git a/app/test/testvariablesmanager.cpp b/app/test/testvariablesmanager.cpp index e7bda0498..cd191c431 100644 --- a/app/test/testvariablesmanager.cpp +++ b/app/test/testvariablesmanager.cpp @@ -84,7 +84,7 @@ void TestVariablesManager::testPositionVariables() evaluateExpression( QStringLiteral( "@position_gps_antenna_height" ), QStringLiteral( "0.000" ), &context ); evaluateExpression( QStringLiteral( "@position_provider_address" ), QStringLiteral( "AA:AA:FF:AA:00:10" ), &context ); evaluateExpression( QStringLiteral( "@position_provider_name" ), QStringLiteral( "testBluetoothProvider" ), &context ); - evaluateExpression( QStringLiteral( "@position_provider_type" ), QStringLiteral( "external" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_type" ), QStringLiteral( "external_bt" ), &context ); mAppSettings->setGpsAntennaHeight( 1.6784 ); pos.verticalSpeed = 1.345; diff --git a/gallery/positionkit.h b/gallery/positionkit.h index 059a3e737..bab5f6dfc 100644 --- a/gallery/positionkit.h +++ b/gallery/positionkit.h @@ -61,7 +61,7 @@ class PositionKit : public QObject private: QString pProviderName = "Gps Source is ok!"; - QString pProviderType = "external"; + QString pProviderType = "external_bt"; QString pProviderMessage = "Connected"; QString pStateMessage = "Message"; QString pLastRead = "17:19:08 CEST"; From 9d2f8e395c3513a436687b4512d9476a6f1a9c75 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 30 Jan 2026 15:39:50 +0100 Subject: [PATCH 02/30] Fix UDP issues --- .../providers/networkpositionprovider.cpp | 143 ++++++++++++------ .../providers/networkpositionprovider.h | 15 +- 2 files changed, 103 insertions(+), 55 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index d96b3bcc6..857bb63aa 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -9,12 +9,16 @@ #include "networkpositionprovider.h" +#include +#include + #include "coreutils.h" static int ONE_SECOND_MS = 1000; NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ), + mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) { const QStringList targetAddress = addr.split( ":" ); mTargetAddress = targetAddress.at( 0 ); @@ -23,36 +27,38 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt mTcpSocket = std::make_unique(); mUdpSocket = std::make_unique(); -// only one socket will be active depending on, which will find host, the other will be closed -// connect(mTcpSocket.get(), &QAbstractSocket::hostFound, this, [this] -// { -// CoreUtils::log("NetworkPositionProvider", "TCP socket found host, aborting UDP socket"); -// mUdpSocket->abort(); -// }); -// connect(mUdpSocket.get(), &QAbstractSocket::hostFound, this, [this] -// { -// CoreUtils::log("NetworkPositionProvider", "UDP socket found host, aborting TCP socket"); -// mTcpSocket->abort(); -// }); - - connect( mTcpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); - connect( mUdpSocket.get(), &QAbstractSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + connect( mTcpSocket.get(), &QTcpSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); + connect( mUdpSocket.get(), &QUdpSocket::readyRead, this, &NetworkPositionProvider::positionUpdateReceived ); - connect( mTcpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); - connect( mUdpSocket.get(), &QAbstractSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + connect( mTcpSocket.get(), &QTcpSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); + connect( mUdpSocket.get(), &QUdpSocket::stateChanged, this, &NetworkPositionProvider::socketStateChanged ); mReconnectTimer.setSingleShot( false ); mReconnectTimer.setInterval( ONE_SECOND_MS ); connect( &mReconnectTimer, &QTimer::timeout, this, &NetworkPositionProvider::reconnectTimeout ); + mUdpReconnectTimer.setSingleShot( true ); + connect( &mUdpReconnectTimer, &QTimer::timeout, this, [this] + { + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); + if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + } ); NetworkPositionProvider::startUpdates(); } void NetworkPositionProvider::startUpdates() { + // TODO: QHostAddress doesn't support hostname lookup (QHostInfo does) CoreUtils::log( "NetworkPositionProvider", "Connecting to host..." ); mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); - mUdpSocket->connectToHost( mTargetAddress, mTargetPort ); + mUdpSocket->bind( QHostAddress::LocalHost, mTargetPort ); + mUdpReconnectTimer.start( ReconnectDelay::ExtraLongDelay ); } void NetworkPositionProvider::stopUpdates() @@ -75,37 +81,92 @@ void NetworkPositionProvider::closeProvider() void NetworkPositionProvider::positionUpdateReceived() { - const QAbstractSocket *socket = dynamic_cast( sender() ); + QAbstractSocket *socket = dynamic_cast( sender() ); const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); CoreUtils::log( "NetworkPositionProvider", QStringLiteral( "%1 socket has received new data!" ).arg( socketTypeToString ) ); - if ( mTcpSocket->isValid() || mUdpSocket->isValid() ) + + // if udp is not connected to the host yet, connect + // this approach will let us use QIODevice functions for both sockets + if ( socket->socketType() == QAbstractSocket::UdpSocket && mUdpSocket->state() != QAbstractSocket::ConnectedState ) { - const QByteArray rawNmeaData = activeSocket()->readAll(); - const QString nmeaData( rawNmeaData ); - const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + mUdpReconnectTimer.stop(); - emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + // if by any chance we showed wrong message in the status like "no connection", fix it here + // we know the connection is working because we just received data from the device + setState( tr( "Connected" ), State::Connected ); + + QHostAddress peerAddress; + int peerPort; + // process the incoming data as it will break the signal emitting if unprocessed + while ( mUdpSocket->hasPendingDatagrams() ) + { + QNetworkDatagram datagram = mUdpSocket->receiveDatagram(); + peerAddress = datagram.senderAddress(); + peerPort = datagram.senderPort(); + const QByteArray rawNmeaData = datagram.data(); + const QString nmeaData( rawNmeaData ); + const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + } + + // "connect" to peer if we are not already connecting + if (mUdpSocket->state() != QAbstractSocket::ConnectedState && mUdpSocket->state() != QAbstractSocket::ConnectingState) + { + mUdpSocket->connectToHost( peerAddress.toString(), peerPort ); + } + return; } + + // stop the UDP silence timer, we just received data + // kills the timer when the app was minimized, and we were able to reconnect in the meantime + if ( socket->socketType() == QAbstractSocket::UdpSocket ) + { + mUdpReconnectTimer.stop(); + } + + // if by any chance we showed wrong message in the status like "no connection", fix it here + // we know the connection is working because we just received data from the device + setState( tr( "Connected" ), State::Connected ); + + const QByteArray rawNmeaData = socket->readAll(); + const QString nmeaData( rawNmeaData ); + const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + + emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); } void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketState state ) { - if ( state == QAbstractSocket::SocketState::ConnectingState || state == QAbstractSocket::SocketState::HostLookupState ) + const QAbstractSocket *socket = dynamic_cast( sender() ); + + if ( state == QAbstractSocket::ConnectingState || state == QAbstractSocket::HostLookupState ) { setState( tr( "Connecting to %1" ).arg( mProviderName ), State::Connecting ); } - else if ( state == QAbstractSocket::SocketState::ConnectedState ) + // Only with TCP we can be sure in ConnectedState that we are connected, with UDP we wait until the first datagram arrives + else if ( state == QAbstractSocket::ConnectedState && socket->socketType() == QAbstractSocket::TcpSocket ) { setState( tr( "Connected" ), State::Connected ); } - else if ( state == QAbstractSocket::SocketState::UnconnectedState ) + else if ( state == QAbstractSocket::UnconnectedState ) { - setState( tr( "No connection" ), State::NoConnection ); - startReconnectTimer(); - // let's also invalidate current position since we no longer have connection - emit positionChanged( GeoPosition() ); + const bool isUdpSocketListening = mUdpSocket->state() == QAbstractSocket::ConnectedState || mUdpSocket->state() == QAbstractSocket::BoundState || mUdpReconnectTimer.isActive(); + if ( socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + else if (socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive) + { + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } } - const QAbstractSocket *socket = dynamic_cast( sender() ); + const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); const QString stateToString = QMetaEnum::fromType().valueToKey( state ); CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "%1 Socket changed state, code: %2" ).arg( socketTypeToString, stateToString ) ); @@ -125,22 +186,6 @@ void NetworkPositionProvider::reconnectTimeout() } } -QAbstractSocket *NetworkPositionProvider::activeSocket() const -{ - if ( mTcpSocket->state() >= QAbstractSocket::ConnectingState && mTcpSocket->state() <= QAbstractSocket::BoundState ) - { - return mTcpSocket.get(); - } - - if ( mUdpSocket->state() >= QAbstractSocket::ConnectingState && mUdpSocket->state() <= QAbstractSocket::BoundState ) - { - return mUdpSocket.get(); - } - -//TODO: wouldn't it be better to guard against nullptr in caller and return nullptr here? - return new QAbstractSocket( QAbstractSocket::UnknownSocketType, nullptr ); -} - void NetworkPositionProvider::reconnect() { mReconnectTimer.stop(); @@ -160,7 +205,7 @@ void NetworkPositionProvider::startReconnectTimer() mReconnectTimer.start(); -// first time do reconnect in short time, then each other in long time + // first time do reconnect in short time, then each other in long time if ( mReconnectDelay == NetworkPositionProvider::ShortDelay ) { mReconnectDelay = NetworkPositionProvider::LongDelay; diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index 5a8755417..d14b458a0 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -22,11 +22,12 @@ class NetworkPositionProvider : public AbstractPositionProvider { Q_OBJECT -// signalizes in how many [ms] we will try to reconnect to GPS again + // signalizes in how many [ms] we will try to reconnect to GPS again enum ReconnectDelay { - ShortDelay = 3000, - LongDelay = 5000 + ShortDelay = 3000, // 3 secs + LongDelay = 5000, // 5 secs + ExtraLongDelay = 10000 // 10 secs }; public: @@ -37,14 +38,15 @@ class NetworkPositionProvider : public AbstractPositionProvider void closeProvider() override; public slots: + // processes the received nmea data and emits new position void positionUpdateReceived(); + // changes the provider state depending on the socket states void socketStateChanged( QAbstractSocket::SocketState state ); + // checks if enough time passed since last reconnect and triggers it if necessary void reconnectTimeout(); private: -// utility function, which should return the socket in use - QAbstractSocket *activeSocket() const; -// trigger the reconnection flow for both sockets + // trigger the reconnection flow for both sockets void reconnect(); // start the reconnection timeout void startReconnectTimer(); @@ -55,6 +57,7 @@ class NetworkPositionProvider : public AbstractPositionProvider int mReconnectDelay = ReconnectDelay::ShortDelay; // in how many [ms] we will try to reconnect again int mSecondsLeftToReconnect; // how many seconds are left to reconnect. Reconnects if less than or equal to one QTimer mReconnectTimer; // timer that times out each second and lowers the mSecondsLeftToReconnect by one + QTimer mUdpReconnectTimer; // timer that times out after ExtraLongDelay and triggers reconnect QString mTargetAddress; // IP address or hostname of the receiver int mTargetPort; // active port of the receiver From 75357b558f8bd3b877339c885eb6424086468f67 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 30 Jan 2026 22:22:43 +0100 Subject: [PATCH 03/30] Fix external provider recreation bug --- app/position/positionkit.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 1e0be92ba..d46d913a5 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -233,9 +233,9 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app if ( providerData[1] == providerId ) { providerName = providerData[0].toString(); - if ( !providerData.at( 3 ).isNull() ) + if ( !providerData.at( 2 ).isNull() ) { - providerType = providerData[3].toString(); + providerType = providerData[2].toString(); } } } From 389492f6fb34e218ee411b1e60b4e8a959fef26d Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Fri, 30 Jan 2026 23:11:52 +0100 Subject: [PATCH 04/30] Fix another restart provider bug --- app/position/providers/positionprovidersmodel.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index b51373f2b..c9a91b6ea 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -197,10 +197,12 @@ void PositionProvidersModel::setAppSettings( AppSettings *as ) } PositionProvider provider; + const QString providerType = providerData[2].isNull() ? QStringLiteral( "external_bt" ) : QStringLiteral( "external_ip" ); + const QString deviceDesc = providerType == QStringLiteral( "external_bt" ) ? tr( " Bluetooth device" ) : tr( " Network device" ); provider.name = providerData[0].toString(); provider.providerId = providerData[1].toString(); - provider.description = provider.providerId + " " + tr( "Bluetooth device" ); - provider.providerType = "external"; + provider.description = provider.providerId + deviceDesc; + provider.providerType = providerType; mProviders.append( provider ); } From ba8b80109312c5fcbb7201ccae03a7aa8f1f2e41 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Mon, 16 Feb 2026 18:58:16 +0100 Subject: [PATCH 05/30] Add Alias field for network provider --- app/qml/gps/MMPositionProviderPage.qml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 5ff5e5964..41d6e4180 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -177,8 +177,6 @@ MMComponents.MMPage { MMComponents.MMDrawerDialog { id: networkProviderInfoDrawer - signal confirmButtonClicked(string address, int port) - title: qsTr("Network provider setup") imageSource: __style.externalGpsGreenImage description: qsTr( "To connect to the external device please specify the IP address and port below." ) @@ -189,6 +187,16 @@ MMComponents.MMPage { width: parent.width spacing: __style.spacing20 + MMInputs.MMTextInput { + id: aliasInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr("Device alias") + placeholderText: qsTr("Green device") + } + MMInputs.MMTextInput { id: ipAddressInput @@ -210,16 +218,13 @@ MMComponents.MMPage { } } - onPrimaryButtonClicked: { + onPrimaryButtonClicked: function() { close() - confirmButtonClicked(ipAddressInput.text, portInput.text) - } - onConfirmButtonClicked: function( address, port ) { - const deviceAddress = address + ":" + port - __positionKit.positionProvider = __positionKit.constructProvider( "external_ip", deviceAddress, "Network provider" ) + const deviceAddress = ipAddressInput.text + ":" + portInput.text + __positionKit.positionProvider = __positionKit.constructProvider( "external_ip", deviceAddress, aliasInput.text ) - providersModel.addProvider( "Network provider", deviceAddress, "external_ip" ) + providersModel.addProvider( aliasInput.text, deviceAddress, "external_ip" ) } } From 68d463a0bb881ce30363b15fd663ba035f5718ed Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 14:32:30 +0100 Subject: [PATCH 06/30] Refactor NmeaParser --- app/CMakeLists.txt | 2 ++ .../providers/bluetoothpositionprovider.cpp | 11 ------- .../providers/bluetoothpositionprovider.h | 23 ++----------- .../providers/networkpositionprovider.cpp | 24 +++++++------- app/position/providers/nmeaparser.cpp | 23 +++++++++++++ app/position/providers/nmeaparser.h | 32 +++++++++++++++++++ 6 files changed, 72 insertions(+), 43 deletions(-) create mode 100644 app/position/providers/nmeaparser.cpp create mode 100644 app/position/providers/nmeaparser.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index ed7a78f83..00c0041cc 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -35,6 +35,7 @@ set(MM_SRCS position/providers/networkpositionprovider.cpp position/providers/positionprovidersmodel.cpp position/providers/simulatedpositionprovider.cpp + position/providers/nmeaparser.cpp position/tracking/abstracttrackingbackend.cpp position/tracking/internaltrackingbackend.cpp position/tracking/positiontrackinghighlight.cpp @@ -126,6 +127,7 @@ set(MM_HDRS position/providers/networkpositionprovider.h position/providers/positionprovidersmodel.h position/providers/simulatedpositionprovider.h + position/providers/nmeaparser.h position/tracking/abstracttrackingbackend.h position/tracking/internaltrackingbackend.h position/tracking/positiontrackinghighlight.h diff --git a/app/position/providers/bluetoothpositionprovider.cpp b/app/position/providers/bluetoothpositionprovider.cpp index 6303bb37d..9c8bbe1c5 100644 --- a/app/position/providers/bluetoothpositionprovider.cpp +++ b/app/position/providers/bluetoothpositionprovider.cpp @@ -19,17 +19,6 @@ #include #endif -NmeaParser::NmeaParser() : QgsNmeaConnection( new QBluetoothSocket() ) -{ -} - -QgsGpsInformation NmeaParser::parseNmeaString( const QString &nmeaString ) -{ - mStringBuffer = nmeaString; - processStringBuffer(); - return mLastGPSInformation; -} - BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent ) : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, parent ) , mTargetAddress( addr ) diff --git a/app/position/providers/bluetoothpositionprovider.h b/app/position/providers/bluetoothpositionprovider.h index 4670131da..ad60d19a9 100644 --- a/app/position/providers/bluetoothpositionprovider.h +++ b/app/position/providers/bluetoothpositionprovider.h @@ -10,29 +10,12 @@ #ifndef BLUETOOTHPOSITIONPROVIDER_H #define BLUETOOTHPOSITIONPROVIDER_H -#include "abstractpositionprovider.h" - -#include "qgsnmeaconnection.h" - #include #include #include -/** - * NmeaParser is a big hack how to reuse QGIS NmeaConnection function in order to (a) keep ownership of bluetooth - * socket, (b) do not have multiple unique_ptrs holding the same pointer and to avoid some possible crashes. - * - * Note: This way of reusing makes the parser highly dependent on QgsNmeaConnection class and any change inside the class - * can lead to misbehavior's. See implementation of QgsNmeaConnection and QgsGpsConnection for more details. - */ -class NmeaParser : public QgsNmeaConnection -{ - public: - NmeaParser(); - - // Takes nmea string and returns gps position - QgsGpsInformation parseNmeaString( const QString &nmeaString ); -}; +#include "abstractpositionprovider.h" +#include "nmeaparser.h" /** * BluetoothPositionProvider initiates connection to bluetooth device @@ -51,7 +34,7 @@ class BluetoothPositionProvider : public AbstractPositionProvider }; public: - BluetoothPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); + BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent = nullptr ); ~BluetoothPositionProvider() override; void startUpdates() override; diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 857bb63aa..b2d0af2fa 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -38,16 +38,16 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt connect( &mReconnectTimer, &QTimer::timeout, this, &NetworkPositionProvider::reconnectTimeout ); mUdpReconnectTimer.setSingleShot( true ); connect( &mUdpReconnectTimer, &QTimer::timeout, this, [this] + { + CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); + if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) { - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); - if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) - { - setState( tr( "No connection" ), State::NoConnection ); - startReconnectTimer(); - // let's also invalidate current position since we no longer have connection - emit positionChanged( GeoPosition() ); - } - } ); + setState( tr( "No connection" ), State::NoConnection ); + startReconnectTimer(); + // let's also invalidate current position since we no longer have connection + emit positionChanged( GeoPosition() ); + } + } ); NetworkPositionProvider::startUpdates(); } @@ -110,7 +110,7 @@ void NetworkPositionProvider::positionUpdateReceived() } // "connect" to peer if we are not already connecting - if (mUdpSocket->state() != QAbstractSocket::ConnectedState && mUdpSocket->state() != QAbstractSocket::ConnectingState) + if ( mUdpSocket->state() != QAbstractSocket::ConnectedState && mUdpSocket->state() != QAbstractSocket::ConnectingState ) { mUdpSocket->connectToHost( peerAddress.toString(), peerPort ); } @@ -151,14 +151,14 @@ void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketS else if ( state == QAbstractSocket::UnconnectedState ) { const bool isUdpSocketListening = mUdpSocket->state() == QAbstractSocket::ConnectedState || mUdpSocket->state() == QAbstractSocket::BoundState || mUdpReconnectTimer.isActive(); - if ( socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive) + if ( socket->socketType() == QAbstractSocket::TcpSocket && !isUdpSocketListening && QApplication::applicationState() == Qt::ApplicationActive ) { setState( tr( "No connection" ), State::NoConnection ); startReconnectTimer(); // let's also invalidate current position since we no longer have connection emit positionChanged( GeoPosition() ); } - else if (socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive) + else if ( socket->socketType() == QAbstractSocket::UdpSocket && QApplication::applicationState() == Qt::ApplicationActive ) { setState( tr( "No connection" ), State::NoConnection ); startReconnectTimer(); diff --git a/app/position/providers/nmeaparser.cpp b/app/position/providers/nmeaparser.cpp new file mode 100644 index 000000000..69a8f1015 --- /dev/null +++ b/app/position/providers/nmeaparser.cpp @@ -0,0 +1,23 @@ +/*************************************************************************** +* * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "nmeaparser.h" + +#include + +NmeaParser::NmeaParser() : QgsNmeaConnection( new QBuffer() ) +{ +} + +QgsGpsInformation NmeaParser::parseNmeaString( const QString &nmeaString ) +{ + mStringBuffer = nmeaString; + processStringBuffer(); + return mLastGPSInformation; +} \ No newline at end of file diff --git a/app/position/providers/nmeaparser.h b/app/position/providers/nmeaparser.h new file mode 100644 index 000000000..fc47d7e82 --- /dev/null +++ b/app/position/providers/nmeaparser.h @@ -0,0 +1,32 @@ +/*************************************************************************** +* * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef NMEAPARSER_H +#define NMEAPARSER_H + +#include + +/** + * NmeaParser is a big hack how to reuse QGIS NmeaConnection function in order to (a) keep ownership of bluetooth + * socket, (b) do not have multiple unique_ptrs holding the same pointer and to avoid some possible crashes. + * + * Note: This way of reusing makes the parser highly dependent on QgsNmeaConnection class and any change inside the class + * can lead to misbehavior's. See implementation of QgsNmeaConnection and QgsGpsConnection for more details. + */ +class NmeaParser : public QgsNmeaConnection +{ + public: + NmeaParser(); + +// Takes nmea string and returns gps position + QgsGpsInformation parseNmeaString( const QString &nmeaString ); +}; + + +#endif //NMEAPARSER_H \ No newline at end of file From c0f609f260f32761307547a0de483bdcc9390dcc Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 14:48:12 +0100 Subject: [PATCH 07/30] Fix breaking bluetooth include --- app/position/providers/networkpositionprovider.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index d14b458a0..794f63458 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -13,9 +13,10 @@ #include #include +#include #include "abstractpositionprovider.h" -#include "bluetoothpositionprovider.h" +#include "nmeaparser.h" class NetworkPositionProvider : public AbstractPositionProvider From 5ae1d6ca44e4641e521103793da16e31b760401f Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 17:47:41 +0100 Subject: [PATCH 08/30] Update tests --- app/test/testposition.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 740ef6f43..0b8ec3567 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -257,7 +257,7 @@ void TestPosition::testPositionProviderKeysInSettings() rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests #ifdef HAVE_BLUETOOTH - positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); + positionKit->setPositionProvider( PositionKit::constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); QCOMPARE( positionKit->positionProvider()->name(), "testProviderA" ); @@ -266,7 +266,7 @@ void TestPosition::testPositionProviderKeysInSettings() QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "AA:BB:CC:DD:EE:FF" ); #endif - positionKit->setPositionProvider( positionKit->constructProvider( "internal", "devicegps" ) ); + positionKit->setPositionProvider( PositionKit::constructProvider( "internal", "devicegps" ) ); QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "devicegps" ); @@ -280,19 +280,27 @@ void TestPosition::testPositionProviderKeysInSettings() providersModel.setAppSettings( &appSettings ); providersModel.addProvider( "testProviderB", "AA:00:11:22:23:44", "external_bt" ); + providersModel.addProvider( "testProviderC", "localhost:9000", "external_ip" ); - // app settings should have one saved provider - testProviderB + // app settings should have two saved providers - testProviderB & testProviderC QVariantList providers = appSettings.savedPositionProviders(); - QCOMPARE( providers.count(), 1 ); // we have one (external) provider - QCOMPARE( providers.at( 0 ).toList().count(), 2 ); // the provider has two properties + QCOMPARE( providers.count(), 2 ); // we have two (external) providers + QCOMPARE( providers.at( 0 ).toList().count(), 3 ); // the provider has two properties QVariantList providerData = providers.at( 0 ).toList(); QCOMPARE( providerData.at( 0 ).toString(), "testProviderB" ); QCOMPARE( providerData.at( 1 ).toString(), "AA:00:11:22:23:44" ); + QCOMPARE( providerData.at( 2 ).toString(), "external_bt" ); + + providerData = providers.at( 1 ).toList(); + QCOMPARE( providerData.at( 0 ).toString(), "testProviderC" ); + QCOMPARE( providerData.at( 1 ).toString(), "localhost:9000" ); + QCOMPARE( providerData.at( 2 ).toString(), "external_ip" ); // remove that provider providersModel.removeProvider( "AA:00:11:22:23:44" ); + providersModel.removeProvider( "localhost:9000" ); providers = appSettings.savedPositionProviders(); From 57e07104b695f426957e45656c51a0d65673dcd0 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 18:17:33 +0100 Subject: [PATCH 09/30] Fix merge leftovers --- app/position/positionkit.cpp | 2 +- app/position/providers/bluetoothpositionprovider.cpp | 4 ++-- app/position/providers/networkpositionprovider.cpp | 4 ++-- app/position/providers/networkpositionprovider.h | 2 +- app/test/testposition.cpp | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index d46d913a5..3026db294 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -145,7 +145,7 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c if ( providerType == QStringLiteral( "external_ip" ) ) { - AbstractPositionProvider *provider = new NetworkPositionProvider( id, name ); + AbstractPositionProvider *provider = new NetworkPositionProvider( id, name, *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } diff --git a/app/position/providers/bluetoothpositionprovider.cpp b/app/position/providers/bluetoothpositionprovider.cpp index 9c8bbe1c5..380c595fe 100644 --- a/app/position/providers/bluetoothpositionprovider.cpp +++ b/app/position/providers/bluetoothpositionprovider.cpp @@ -19,8 +19,8 @@ #include #endif -BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, parent ) +BluetoothPositionProvider::BluetoothPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_bt" ), name, positionTransformer, parent ) , mTargetAddress( addr ) { mSocket = std::unique_ptr( new QBluetoothSocket( QBluetoothServiceInfo::RfcommProtocol ) ); diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index b2d0af2fa..93189d3ac 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -16,8 +16,8 @@ static int ONE_SECOND_MS = 1000; -NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent ) - : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, parent ), +NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) + : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, positionTransformer, parent ), mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) { const QStringList targetAddress = addr.split( ":" ); diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index 794f63458..9d4bc080f 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -32,7 +32,7 @@ class NetworkPositionProvider : public AbstractPositionProvider }; public: - explicit NetworkPositionProvider( const QString &addr, const QString &name, QObject *parent = nullptr ); + NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); void startUpdates() override; void stopUpdates() override; diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 0b8ec3567..d3b78c73b 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -257,7 +257,7 @@ void TestPosition::testPositionProviderKeysInSettings() rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests #ifdef HAVE_BLUETOOTH - positionKit->setPositionProvider( PositionKit::constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); + positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); QCOMPARE( positionKit->positionProvider()->name(), "testProviderA" ); @@ -266,7 +266,7 @@ void TestPosition::testPositionProviderKeysInSettings() QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "AA:BB:CC:DD:EE:FF" ); #endif - positionKit->setPositionProvider( PositionKit::constructProvider( "internal", "devicegps" ) ); + positionKit->setPositionProvider( positionKit->constructProvider( "internal", "devicegps" ) ); QCOMPARE( rawSettings.value( CoreUtils::QSETTINGS_APP_GROUP_NAME + "/activePositionProviderId" ).toString(), "devicegps" ); From 0220d3d7f0347adf103653b20fea882d48567a44 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 17 Feb 2026 18:19:25 +0100 Subject: [PATCH 10/30] Remove debug logs --- app/position/providers/networkpositionprovider.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 93189d3ac..ef5b52654 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -12,8 +12,6 @@ #include #include -#include "coreutils.h" - static int ONE_SECOND_MS = 1000; NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) @@ -39,7 +37,6 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt mUdpReconnectTimer.setSingleShot( true ); connect( &mUdpReconnectTimer, &QTimer::timeout, this, [this] { - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "UDP socket no data received in threshold, triggering reconnect..." ) ); if ( mTcpSocket->state() != QAbstractSocket::ConnectedState ) { setState( tr( "No connection" ), State::NoConnection ); @@ -55,7 +52,6 @@ NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QSt void NetworkPositionProvider::startUpdates() { // TODO: QHostAddress doesn't support hostname lookup (QHostInfo does) - CoreUtils::log( "NetworkPositionProvider", "Connecting to host..." ); mTcpSocket->connectToHost( mTargetAddress, mTargetPort ); mUdpSocket->bind( QHostAddress::LocalHost, mTargetPort ); mUdpReconnectTimer.start( ReconnectDelay::ExtraLongDelay ); @@ -83,7 +79,6 @@ void NetworkPositionProvider::positionUpdateReceived() { QAbstractSocket *socket = dynamic_cast( sender() ); const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); - CoreUtils::log( "NetworkPositionProvider", QStringLiteral( "%1 socket has received new data!" ).arg( socketTypeToString ) ); // if udp is not connected to the host yet, connect // this approach will let us use QIODevice functions for both sockets @@ -169,7 +164,6 @@ void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketS const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); const QString stateToString = QMetaEnum::fromType().valueToKey( state ); - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "%1 Socket changed state, code: %2" ).arg( socketTypeToString, stateToString ) ); } void NetworkPositionProvider::reconnectTimeout() @@ -192,8 +186,6 @@ void NetworkPositionProvider::reconnect() setState( tr( "Reconnecting" ), State::Connecting ); - CoreUtils::log( QStringLiteral( "NetworkPositionProvider" ), QStringLiteral( "Reconnecting to %1" ).arg( mProviderName ) ); - stopUpdates(); startUpdates(); } From 2cd278e2109637dbee33e90c9f6ce5b4cb67fbab Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 09:02:33 +0100 Subject: [PATCH 11/30] Fix android merge error --- app/position/positionkit.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 3026db294..ee55ed6f1 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -161,7 +161,7 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c #ifdef ANDROID if ( id == QStringLiteral( "android_fused" ) || id == QStringLiteral( "android_gps" ) ) { - bool fused = ( id == QStringLiteral( "android_fused" ) ); + const bool fused = ( id == QStringLiteral( "android_fused" ) ); if ( fused && !AndroidPositionProvider::isFusedAvailable() ) { // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? From 1d57f50a63cd6644625d663d2926363eb76b4899 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 10:49:32 +0100 Subject: [PATCH 12/30] Add position processing for network provider --- app/position/positiontransformer.cpp | 5 +++++ app/position/positiontransformer.h | 8 ++++++++ app/position/providers/networkpositionprovider.cpp | 5 ++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/position/positiontransformer.cpp b/app/position/positiontransformer.cpp index 3d5dd559c..52071dd16 100644 --- a/app/position/positiontransformer.cpp +++ b/app/position/positiontransformer.cpp @@ -50,6 +50,11 @@ GeoPosition PositionTransformer::processBluetoothPosition( GeoPosition geoPositi return geoPosition; } +GeoPosition PositionTransformer::processNetworkPosition( const GeoPosition &geoPosition ) +{ + return processBluetoothPosition( geoPosition ); +} + GeoPosition PositionTransformer::processAndroidPosition( GeoPosition geoPosition ) { if ( geoPosition.elevation != std::numeric_limits::quiet_NaN() ) diff --git a/app/position/positiontransformer.h b/app/position/positiontransformer.h index 87e4a2c6a..0bac4de0f 100644 --- a/app/position/positiontransformer.h +++ b/app/position/positiontransformer.h @@ -49,6 +49,14 @@ class PositionTransformer : QObject */ GeoPosition processBluetoothPosition( GeoPosition geoPosition ); + /** + * Transform the elevation if the user sets custom vertical CRS. The elevation gets recalculated to ellipsoid elevation + * and then back to orthometric based on specified CRS. + * \note This method should be used only with NetworkPositionProvider to mitigate unnecessary transformations + * \return Copy of passed geoPosition with processed elevation and elevation separation. + */ + GeoPosition processNetworkPosition( const GeoPosition &geoPosition ); + /** * Transform the elevation from EPSG:4979 (WGS84 (EPSG:4326) + ellipsoidal height) to specified geoid model * (by default EPSG:9707 (WGS84 + EGM96)) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index ef5b52654..fbe533b65 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -101,7 +101,9 @@ void NetworkPositionProvider::positionUpdateReceived() const QByteArray rawNmeaData = datagram.data(); const QString nmeaData( rawNmeaData ); const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); - emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + GeoPosition transformedPosition = mPositionTransformer->processNetworkPosition( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + + emit positionChanged( transformedPosition ); } // "connect" to peer if we are not already connecting @@ -126,6 +128,7 @@ void NetworkPositionProvider::positionUpdateReceived() const QByteArray rawNmeaData = socket->readAll(); const QString nmeaData( rawNmeaData ); const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); + GeoPosition transformedPosition = mPositionTransformer->processNetworkPosition( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); } From 880ce504bba72ee36911dd66208db0e2f82fa324 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 11:12:47 +0100 Subject: [PATCH 13/30] Add processing tests --- app/test/testposition.cpp | 67 +++++++++++++++++++++++++++++++++++++++ app/test/testposition.h | 1 + 2 files changed, 68 insertions(+) diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index d3b78c73b..b7c4a75b6 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -770,6 +770,73 @@ void TestPosition::testPositionTransformerInternalDesktopPosition() QVERIFY( qgsDoubleNear( newPosition.elevation, 171.3 ) ); } +void TestPosition::testPositionTransformerNetworkPosition() +{ + // prepare position transformers + // WGS84 + ellipsoid + QgsCoordinateReferenceSystem ellipsoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 4979 ); + // WGS84 + EGM96 + QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); + PositionTransformer passThroughTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); + PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); + +#ifdef HAVE_BLUETOOTH + // mini file contains only minimal info like position and date + QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; + QFile miniNmeaFile( miniNmeaPositionFilePath ); + miniNmeaFile.open( QFile::ReadOnly ); + + QVERIFY( miniNmeaFile.isOpen() ); + + NmeaParser parser; + QgsGpsInformation position = parser.parseNmeaString( miniNmeaFile.readAll() ); + GeoPosition geoPosition = GeoPosition::fromQgsGpsInformation( position ); + + QVERIFY( qgsDoubleNear( geoPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( geoPosition.longitude, 17.1064 ) ); + QCOMPARE( geoPosition.elevation, 171.3 ); + QCOMPARE( geoPosition.elevation_diff, std::numeric_limits::quiet_NaN() ); +#else + GeoPosition geoPosition; + geoPosition.latitude = 48.10305; + geoPosition.longitude = 17.1064; + geoPosition.elevation = 171.3; +#endif + + // transform with pass through disabled and missing elevation separation + GeoPosition newPosition = positionTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QCOMPARE( newPosition.elevation, 171.3 ); + QCOMPARE( newPosition.elevation_diff, std::numeric_limits::quiet_NaN() ); + + // transform with pass through enabled and missing elevation separation + newPosition = passThroughTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QCOMPARE( newPosition.elevation, 171.3 ); + QCOMPARE( newPosition.elevation_diff, std::numeric_limits::quiet_NaN() ); + + // transform with pass through enabled and elevation separation + geoPosition.elevation_diff = 40; + newPosition = passThroughTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QCOMPARE( newPosition.elevation, 171.3 ); + QCOMPARE( newPosition.elevation_diff, 40 ); + + // transform with pass through disabled and elevation separation + newPosition = positionTransformer.processNetworkPosition( geoPosition ); + + QVERIFY( qgsDoubleNear( newPosition.latitude, 48.10305 ) ); + QVERIFY( qgsDoubleNear( newPosition.longitude, 17.1064 ) ); + QVERIFY( qgsDoubleNear( newPosition.elevation, 167.53574931171875 ) ); + QVERIFY( qgsDoubleNear( newPosition.elevation_diff, 43.764250688281265 ) ); +} + void TestPosition::testPositionTransformerSimulatedPosition() { // prepare position transformers diff --git a/app/test/testposition.h b/app/test/testposition.h index 3636b89c3..40ffd45c6 100644 --- a/app/test/testposition.h +++ b/app/test/testposition.h @@ -44,6 +44,7 @@ class TestPosition: public QObject void testPositionTransformerInternalAndroidPosition(); void testPositionTransformerInternalIosPosition(); void testPositionTransformerInternalDesktopPosition(); + void testPositionTransformerNetworkPosition(); void testPositionTransformerSimulatedPosition(); private: From 3310a84d07a9196c502b360bb425814e64788cc1 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 18 Feb 2026 11:17:27 +0100 Subject: [PATCH 14/30] Fix formatting --- app/test/testposition.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index b7c4a75b6..846c4c227 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -772,7 +772,7 @@ void TestPosition::testPositionTransformerInternalDesktopPosition() void TestPosition::testPositionTransformerNetworkPosition() { - // prepare position transformers +// prepare position transformers // WGS84 + ellipsoid QgsCoordinateReferenceSystem ellipsoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 4979 ); // WGS84 + EGM96 From c5d88052533c980fa7f0d143f0b4a353695daf5c Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 11 Mar 2026 12:58:18 +0100 Subject: [PATCH 15/30] Fix provider removal crash + minor cleanup --- app/position/providers/networkpositionprovider.cpp | 9 +++++---- app/qml/gps/MMPositionProviderPage.qml | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index fbe533b65..b2b552e65 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -73,12 +73,16 @@ void NetworkPositionProvider::closeProvider() { mTcpSocket->close(); mUdpSocket->close(); + if ( mTcpSocket) mTcpSocket->disconnect(); + if ( mUdpSocket) mUdpSocket->disconnect(); + + mUdpReconnectTimer.stop(); + mReconnectTimer.stop(); } void NetworkPositionProvider::positionUpdateReceived() { QAbstractSocket *socket = dynamic_cast( sender() ); - const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); // if udp is not connected to the host yet, connect // this approach will let us use QIODevice functions for both sockets @@ -164,9 +168,6 @@ void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketS emit positionChanged( GeoPosition() ); } } - - const QString socketTypeToString = QMetaEnum::fromType().valueToKey( socket->socketType() ); - const QString stateToString = QMetaEnum::fromType().valueToKey( state ); } void NetworkPositionProvider::reconnectTimeout() diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 41d6e4180..550636f18 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -222,7 +222,7 @@ MMComponents.MMPage { close() const deviceAddress = ipAddressInput.text + ":" + portInput.text - __positionKit.positionProvider = __positionKit.constructProvider( "external_ip", deviceAddress, aliasInput.text ) + PositionKit.positionProvider = PositionKit.constructProvider( "external_ip", deviceAddress, aliasInput.text ) providersModel.addProvider( aliasInput.text, deviceAddress, "external_ip" ) } From c415f608642d8d4b1ede48fc83d83945b47d719b Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Thu, 12 Mar 2026 14:55:47 +0200 Subject: [PATCH 16/30] Added bluetooth and network provider icons Modified the MMIconCheckBoxHorizontal to feature a description if needed Modularised the MMPositionProviderPage Refactored MMBluetoothConnectionDrawer to be used for the network provider as well Changes the connecting and failure images for the network provider Other small changes --- app/icons/Bluetooth.svg | 3 + app/icons/Network.svg | 6 + app/icons/icons.qrc | 2 + app/images/ExternalBluetoothProvider.svg | 274 +++++++++++++++++ app/images/ExternalGpsGreen.svg | 19 -- app/images/ExternalGpsRed.svg | 272 ++++++++++++++++- app/images/ExternalNetworkProvider.svg | 277 ++++++++++++++++++ app/images/images.qrc | 3 +- app/mmstyle.h | 10 +- .../providers/networkpositionprovider.cpp | 4 +- app/qml/CMakeLists.txt | 4 +- .../components/MMIconCheckBoxHorizontal.qml | 61 +++- app/qml/gps/MMAddPositionProviderDrawer.qml | 18 +- ...=> MMExternalProviderConnectionDrawer.qml} | 27 +- app/qml/gps/MMNetworkProviderDrawer.qml | 119 ++++++++ app/qml/gps/MMPositionProviderPage.qml | 188 +++++------- app/qml/gps/MMProviderTypeDrawer.qml | 102 +++++++ gallery/qml.qrc | 2 +- gallery/qml/pages/ChecksPage.qml | 4 +- gallery/qml/pages/DrawerPage.qml | 5 +- gallery/qml/pages/ImagesPage.qml | 3 +- 21 files changed, 1232 insertions(+), 171 deletions(-) create mode 100644 app/icons/Bluetooth.svg create mode 100644 app/icons/Network.svg create mode 100644 app/images/ExternalBluetoothProvider.svg delete mode 100644 app/images/ExternalGpsGreen.svg create mode 100644 app/images/ExternalNetworkProvider.svg rename app/qml/gps/{MMBluetoothConnectionDrawer.qml => MMExternalProviderConnectionDrawer.qml} (75%) create mode 100644 app/qml/gps/MMNetworkProviderDrawer.qml create mode 100644 app/qml/gps/MMProviderTypeDrawer.qml diff --git a/app/icons/Bluetooth.svg b/app/icons/Bluetooth.svg new file mode 100644 index 000000000..f18e6d485 --- /dev/null +++ b/app/icons/Bluetooth.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/icons/Network.svg b/app/icons/Network.svg new file mode 100644 index 000000000..3ff4e7b3d --- /dev/null +++ b/app/icons/Network.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/icons/icons.qrc b/app/icons/icons.qrc index 8d9a78db9..261db4fb3 100644 --- a/app/icons/icons.qrc +++ b/app/icons/icons.qrc @@ -7,6 +7,7 @@ ArrowLinkRight.svg ArrowUp.svg Back.svg + Bluetooth.svg Briefcase.svg Calendar.svg Checkmark.svg @@ -57,6 +58,7 @@ Mouth.svg NaturalResources.svg Next.svg + Network.svg Other.svg Others.svg Personal.svg diff --git a/app/images/ExternalBluetoothProvider.svg b/app/images/ExternalBluetoothProvider.svg new file mode 100644 index 000000000..35f92c7bd --- /dev/null +++ b/app/images/ExternalBluetoothProvider.svg @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/ExternalGpsGreen.svg b/app/images/ExternalGpsGreen.svg deleted file mode 100644 index d5a833bd7..000000000 --- a/app/images/ExternalGpsGreen.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/images/ExternalGpsRed.svg b/app/images/ExternalGpsRed.svg index 9fd5a52d2..67f981956 100644 --- a/app/images/ExternalGpsRed.svg +++ b/app/images/ExternalGpsRed.svg @@ -1,14 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + - + diff --git a/app/images/ExternalNetworkProvider.svg b/app/images/ExternalNetworkProvider.svg new file mode 100644 index 000000000..a2602f99f --- /dev/null +++ b/app/images/ExternalNetworkProvider.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/images/images.qrc b/app/images/images.qrc index b0b772a30..2d2cf69f2 100644 --- a/app/images/images.qrc +++ b/app/images/images.qrc @@ -19,8 +19,9 @@ UploadImage.svg WarnLogoImage.svg NoMapThemesImage.svg + ExternalBluetoothProvider.svg ExternalGpsRed.svg - ExternalGpsGreen.svg + ExternalNetworkProvider.svg NegativeMMSymbol.svg PositiveMMSymbol.svg NeutralMMSymbol.svg diff --git a/app/mmstyle.h b/app/mmstyle.h index 1dc74b4f6..f51f4a567 100644 --- a/app/mmstyle.h +++ b/app/mmstyle.h @@ -112,6 +112,7 @@ class MMStyle: public QObject Q_PROPERTY( QUrl arrowDownIcon READ arrowDownIcon CONSTANT ) Q_PROPERTY( QUrl arrowLinkRightIcon READ arrowLinkRightIcon CONSTANT ) Q_PROPERTY( QUrl arrowUpIcon READ arrowUpIcon CONSTANT ) + Q_PROPERTY( QUrl bluetoothIcon READ bluetoothIcon CONSTANT ) Q_PROPERTY( QUrl backIcon READ backIcon CONSTANT ) Q_PROPERTY( QUrl briefcaseIcon READ briefcaseIcon CONSTANT ) Q_PROPERTY( QUrl calendarIcon READ calendarIcon CONSTANT ) @@ -144,6 +145,7 @@ class MMStyle: public QObject Q_PROPERTY( QUrl mouthIcon READ mouthIcon CONSTANT ) Q_PROPERTY( QUrl naturalResourcesIcon READ naturalResourcesIcon CONSTANT ) Q_PROPERTY( QUrl nextIcon READ nextIcon CONSTANT ) + Q_PROPERTY( QUrl networkIcon READ networkIcon CONSTANT ) Q_PROPERTY( QUrl otherIcon READ otherIcon CONSTANT ) Q_PROPERTY( QUrl othersIcon READ othersIcon CONSTANT ) Q_PROPERTY( QUrl plusIcon READ plusIcon CONSTANT ) @@ -227,8 +229,9 @@ class MMStyle: public QObject Q_PROPERTY( QUrl positionTrackingRunningImage READ positionTrackingRunningImage CONSTANT ) Q_PROPERTY( QUrl positionTrackingStartImage READ positionTrackingStartImage CONSTANT ) Q_PROPERTY( QUrl syncImage READ syncImage CONSTANT ) - Q_PROPERTY( QUrl externalGpsGreenImage READ externalGpsGreenImage CONSTANT ) Q_PROPERTY( QUrl externalGpsRedImage READ externalGpsRedImage CONSTANT ) + Q_PROPERTY( QUrl externalBluetoothGreenImage READ externalBluetoothGreenImage CONSTANT ) + Q_PROPERTY( QUrl externalNetworkGreenImage READ externalNetworkGreenImage CONSTANT ) Q_PROPERTY( QUrl negativeMMSymbolImage READ negativeMMSymbolImage CONSTANT ) Q_PROPERTY( QUrl positiveMMSymbolImage READ positiveMMSymbolImage CONSTANT ) Q_PROPERTY( QUrl neutralMMSymbolImage READ neutralMMSymbolImage CONSTANT ) @@ -424,6 +427,7 @@ class MMStyle: public QObject QUrl arrowLinkRightIcon() const {return QUrl( "qrc:/ArrowLinkRight.svg" );} QUrl arrowUpIcon() const {return QUrl( "qrc:/ArrowUp.svg" );} QUrl backIcon() const {return QUrl( "qrc:/Back.svg" );} + QUrl bluetoothIcon() const {return QUrl( "qrc:/Bluetooth.svg" );} QUrl briefcaseIcon() const {return QUrl( "qrc:/Briefcase.svg" );} QUrl calendarIcon() const {return QUrl( "qrc:/Calendar.svg" );} QUrl checkmarkIcon() const {return QUrl( "qrc:/Checkmark.svg" );} @@ -454,6 +458,7 @@ class MMStyle: public QObject QUrl remoteImageLoadErrorIcon() const {return QUrl( "qrc:/RemoteImageLoadError.svg" );} QUrl mouthIcon() const {return QUrl( "qrc:/Mouth.svg" );} QUrl measurementToolIcon() const {return QUrl( "qrc:/Measure.svg" );} + QUrl networkIcon() const {return QUrl( "qrc:/Network.svg" );} QUrl closeShapeIcon() const {return QUrl( "qrc:/CloseShape.svg" );} QUrl naturalResourcesIcon() const {return QUrl( "qrc:/NaturalResources.svg" );} QUrl nextIcon() const {return QUrl( "qrc:/Next.svg" );} @@ -530,8 +535,9 @@ class MMStyle: public QObject QUrl positionTrackingRunningImage() const {return QUrl( "qrc:/images/PositionTrackingRunning.svg" );} QUrl positionTrackingStartImage() const {return QUrl( "qrc:/images/PositionTrackingStart.svg" );} QUrl syncImage() const {return QUrl( "qrc:/images/SyncImage.svg" );} - QUrl externalGpsGreenImage() const {return QUrl( "qrc:/images/ExternalGpsGreen.svg" );} QUrl externalGpsRedImage() const {return QUrl( "qrc:/images/ExternalGpsRed.svg" );} + QUrl externalBluetoothGreenImage() const {return QUrl( "qrc:/images/ExternalBluetoothProvider.svg" );} + QUrl externalNetworkGreenImage() const {return QUrl( "qrc:/images/ExternalNetworkProvider.svg" );} QUrl negativeMMSymbolImage() const {return QUrl( "qrc:/images/NegativeMMSymbol.svg" );} QUrl positiveMMSymbolImage() const {return QUrl( "qrc:/images/PositiveMMSymbol.svg" );} QUrl neutralMMSymbolImage() const {return QUrl( "qrc:/images/NeutralMMSymbol.svg" );} diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index b2b552e65..d7abb3627 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -73,8 +73,8 @@ void NetworkPositionProvider::closeProvider() { mTcpSocket->close(); mUdpSocket->close(); - if ( mTcpSocket) mTcpSocket->disconnect(); - if ( mUdpSocket) mUdpSocket->disconnect(); + if ( mTcpSocket ) mTcpSocket->disconnect(); + if ( mUdpSocket ) mUdpSocket->disconnect(); mUdpReconnectTimer.stop(); mReconnectTimer.stop(); diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index d9662b019..a2697371d 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -119,9 +119,11 @@ set(MM_QML form/editors/MMFormValueMapEditor.qml form/editors/MMFormValueRelationEditor.qml gps/MMAddPositionProviderDrawer.qml - gps/MMBluetoothConnectionDrawer.qml + gps/MMExternalProviderConnectionDrawer.qml gps/MMGpsDataDrawer.qml + gps/MMNetworkProviderDrawer.qml gps/MMPositionProviderPage.qml + gps/MMProviderTypeDrawer.qml gps/MMStakeoutDrawer.qml gps/MMMeasureDrawer.qml gps/MMSelectionDrawer.qml diff --git a/app/qml/account/components/MMIconCheckBoxHorizontal.qml b/app/qml/account/components/MMIconCheckBoxHorizontal.qml index cd3a02452..f9327bb18 100644 --- a/app/qml/account/components/MMIconCheckBoxHorizontal.qml +++ b/app/qml/account/components/MMIconCheckBoxHorizontal.qml @@ -17,21 +17,25 @@ CheckBox { id: control property string sourceIcon: "" + property string description: "" + property bool showBorder: false property bool small: false - height: (control.small ? 50 : 80) * __dp + height: (description !== "" ? 96 : (control.small ? 50 : 80)) * __dp + + leftPadding: (description !== "" ? iconBgRectangle.x : 0) + iconBgRectangle.width + 30 * __dp + rightPadding: 20 * __dp indicator: Rectangle { id: iconBgRectangle - width: (control.small ? 24 : 40) * __dp - height: (control.small ? 24 : 40) * __dp + width: (control.small ? 24 : (control.description !== "" ? 50 : 40)) * __dp + height: (control.small ? 24 : (control.description !== "" ? 50 : 40)) * __dp x: 20 * __dp y: control.height / 2 - height / 2 radius: width / 2 color: control.checked ? __style.polarColor : __style.lightGreenColor MMIcon { - id: icon size: control.small ? __style.icon16 : __style.icon24 anchors.centerIn: parent source: control.sourceIcon @@ -39,18 +43,49 @@ CheckBox { } } - contentItem: Text { - text: control.text - font: __style.t3 - color: control.checked ? __style.polarColor : __style.nightColor - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - leftPadding: control.indicator.width + 30 * __dp - rightPadding: 20 * __dp + contentItem: Item { + implicitWidth: titleText.implicitWidth + + Text { + id: titleText + visible: control.description === "" + width: parent.width + height: parent.height + text: control.text + font: __style.t3 + color: control.checked ? __style.polarColor : __style.nightColor + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + Column { + id: textColumn + visible: control.description !== "" + anchors.verticalCenter: parent.verticalCenter + width: parent.width + spacing: 10 * __dp + + Text { + width: parent.width + text: control.text + font: __style.t3 + color: control.checked ? __style.polarColor : __style.nightColor + elide: Text.ElideRight + } + + Text { + width: parent.width + text: control.description + font: __style.p6 + color: control.checked ? __style.polarColor : __style.nightColor + elide: Text.ElideRight + } + } } background: Rectangle { radius: __style.radius12 - color: control.checked ? __style.forestColor: __style.polarColor + color: control.checked ? __style.forestColor : __style.polarColor + border.color: showBorder ? ( control.checked ? __style.transparentColor : __style.mediumGreenColor ) : __style.transparentColor } } diff --git a/app/qml/gps/MMAddPositionProviderDrawer.qml b/app/qml/gps/MMAddPositionProviderDrawer.qml index ab4771bcf..5531a52ca 100644 --- a/app/qml/gps/MMAddPositionProviderDrawer.qml +++ b/app/qml/gps/MMAddPositionProviderDrawer.qml @@ -103,16 +103,30 @@ MMComponents.MMListDrawer { Column { width: ListView.view.width - spacing: 0 + spacing: __style.spacing16 MMComponents.MMListSpacer { height: __style.margin40 } + Image { + anchors.horizontalCenter: parent.horizontalCenter + + source: __style.mmSymbolImage + + width: 32 * __dp + height: 32 * __dp + + fillMode: Image.PreserveAspectFit + } + MMComponents.MMText { width: parent.width - text: qsTr( "Looking for devices" ) + "..." + text: qsTr( "Looking for more devices" ) + "..." font: __style.t3 + color: __style.forestColor + + horizontalAlignment: Text.AlignHCenter } } } diff --git a/app/qml/gps/MMBluetoothConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml similarity index 75% rename from app/qml/gps/MMBluetoothConnectionDrawer.qml rename to app/qml/gps/MMExternalProviderConnectionDrawer.qml index 64d5c9af4..92e113bfc 100644 --- a/app/qml/gps/MMBluetoothConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -19,6 +19,7 @@ import "../components" as MMComponents MMComponents.MMDrawer { id: root + property string providerType: "" property var positionProvider: PositionKit.positionProvider property string howToConnectGPSLink: __inputHelp.howToConnectGPSLink @@ -33,11 +34,18 @@ MMComponents.MMDrawer { { return qsTr( "Connected" ) } - else + else if ( rootstate.state === "fail") { - // either NoConnection or WaitingToReconnect return qsTr( "Failed to connect to" ) + " " + ( root.positionProvider ? root.positionProvider.name() : "" ) } + else + { + if( root.providerType === "bluetooth") + return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to.") + else if (root.providerType === "network") + // either NoConnection or WaitingToReconnect + return qsTr( "We were not able to connect to the specified IP address. Please try again later." ) + } } property string connectingSuffixAnimation: "" @@ -45,7 +53,10 @@ MMComponents.MMDrawer { property string descriptionText: { if ( rootstate.state === "working" ) { - return qsTr( "You might be asked to pair your device during this process." ) + if( root.providerType === "bluetooth") + return qsTr( "You might be asked to pair your device during this process." ) + else if( root.providerType === "network") + return qsTr( "This might take a while..." ) } else if ( rootstate.state === "success" ) { @@ -59,7 +70,10 @@ MMComponents.MMDrawer { else { - return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) + if( root.providerType === "bluetooth") + return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) + else if( root.providerType === "network") + return qsTr( "We were not able to connect to the specified IP address. Please try again later." ) } } @@ -69,7 +83,10 @@ MMComponents.MMDrawer { return __style.externalGpsRedImage } else { - return __style.externalGpsGreenImage + if( root.providerType === "bluetooth" ) + return __style.externalBluetoothGreenImage + else if (root.providerType === "network") + return __style.externalNetworkGreenImage } } diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml new file mode 100644 index 000000000..d5632fdec --- /dev/null +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -0,0 +1,119 @@ +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick + +import "../components" as MMComponents +import "../inputs" as MMInputs + +MMComponents.MMDrawer { + id: root + + signal confirmed( string alias, string deviceAddress ) + + drawerHeader.title: qsTr( "Network connection" ) + drawerHeader.titleFont: __style.t2 + + drawerContent: Column { + width: parent.width + spacing: __style.spacing20 + + MMComponents.MMText { + width: parent.width + + text: qsTr( "External receivers can be connected via network in iOS devices. Some of the known devices that support network connection (TCP or UDP) are EOS and Emlid." ) + + font: __style.p5 + color: __style.nightColor + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignJustify + } + + MMInputs.MMTextInput { + id: ipAddressInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr( "IP address" ) + placeholderText: qsTr( "localhost" ) + + onTextEdited: errorMsg = "" + } + + MMInputs.MMTextInput { + id: portInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr( "Port" ) + placeholderText: qsTr( "1234" ) + + textField.inputMethodHints: Qt.ImhDigitsOnly + + onTextEdited: errorMsg = "" + } + + MMInputs.MMTextInput { + id: aliasInput + + width: parent.width + textFieldBackground.color: __style.lightGreenColor + + title: qsTr( "Receiver nickname (optional)" ) + placeholderText: qsTr( "Green device" ) + } + + MMComponents.MMButton { + width: parent.width + + text: qsTr( "Confirm" ) + + onClicked: { + const ip = ipAddressInput.text.trim() + const port = portInput.text.trim() + + const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/ + const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/ + + if ( ip === "" ) { + ipAddressInput.errorMsg = qsTr( "IP address is required" ) + } + else if ( !ipv4Regex.test( ip ) && !hostnameRegex.test( ip ) ) { + ipAddressInput.errorMsg = qsTr( "Enter a valid IP address or hostname" ) + } + else { + ipAddressInput.errorMsg = "" + } + + const portNum = parseInt( port ) + if ( port === "" ) { + portInput.errorMsg = qsTr( "Port is required" ) + } + else if ( !/^\d+$/.test( port ) || portNum < 1 || portNum > 65535 ) { + portInput.errorMsg = qsTr( "Enter a valid port (1–65535)" ) + } + else { + portInput.errorMsg = "" + } + + if ( ipAddressInput.errorMsg !== "" || portInput.errorMsg !== "" ) { + return + } + + const deviceAddress = ip + ":" + port + + root.confirmed( aliasInput.text, deviceAddress ) + root.close() + } + } + } +} diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 550636f18..471e48ba6 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -8,16 +8,12 @@ ***************************************************************************/ import QtQuick -import QtQml.Models -import QtQuick.Controls -import QtQuick.Dialogs import mm 1.0 as MM import MMInput import "../components" as MMComponents import "../dialogs" as MMDialogs -import "../inputs" as MMInputs MMComponents.MMPage { id: root @@ -35,8 +31,6 @@ MMComponents.MMPage { property bool showTopTitle: visibleArea.yPosition * height > ( headerItem.contentHeight / 2 ) - visible: __haveBluetooth - width: parent.width height: parent.height @@ -96,7 +90,7 @@ MMComponents.MMPage { if ( ListView.section === "internal" ) { let ix = providersModel.index( index + 1, 0 ) let type = providersModel.data( ix, MM.PositionProvidersModel.ProviderType ) - if ( type.includes("external") ) return false + if ( type.includes( "external" ) ) return false } return true @@ -137,98 +131,36 @@ MMComponents.MMPage { text: qsTr( "Connect new receiver" ) - onClicked: providerTypeDrawer.open() - } - - MMComponents.MMListDrawer { - id: providerTypeDrawer - - drawerHeader.title: qsTr("Pick the correct provider type") - drawerHeader.titleFont: __style.t2 - - list.model: ListModel { - ListElement { - name: qsTr( "Bluetooth provider" ) - type: "bluetooth" - } - ListElement { - name: qsTr( "Network provider" ) - type: "network" + onClicked: { + if ( __haveBluetooth ) { + providerTypeDrawer.open() } - } - - list.delegate: MMComponents.MMListDelegate { - required property string name - required property string type - - text: name - onClicked: { - providerTypeDrawer.close() - if (type === "bluetooth") { - bluetoothDiscoveryLoader.active = true - } - else if (type === "network"){ - networkProviderInfoDrawer.open() - } + else { + networkProviderDrawer.open() } } } - MMComponents.MMDrawerDialog { - id: networkProviderInfoDrawer - - title: qsTr("Network provider setup") - imageSource: __style.externalGpsGreenImage - description: qsTr( "To connect to the external device please specify the IP address and port below." ) - primaryButton.text: qsTr( "Confirm" ) - primaryButton.type: MMComponents.MMButton.Primary - - additionalContent: Column { - width: parent.width - spacing: __style.spacing20 - - MMInputs.MMTextInput { - id: aliasInput - - width: parent.width - textFieldBackground.color: __style.lightGreenColor - - title: qsTr("Device alias") - placeholderText: qsTr("Green device") - } - - MMInputs.MMTextInput { - id: ipAddressInput - - width: parent.width - textFieldBackground.color: __style.lightGreenColor - - title: qsTr("IP address") - placeholderText: qsTr("localhost") - } - - MMInputs.MMTextInput { - id: portInput - - width: parent.width - textFieldBackground.color: __style.lightGreenColor - - title: qsTr("Port") - placeholderText: qsTr("1234") - } - } + MMProviderTypeDrawer { + id: providerTypeDrawer - onPrimaryButtonClicked: function() { - close() + onBluetoothSelected: bluetoothDiscoveryLoader.active = true + onNetworkSelected: networkProviderDrawer.open() + } - const deviceAddress = ipAddressInput.text + ":" + portInput.text - PositionKit.positionProvider = PositionKit.constructProvider( "external_ip", deviceAddress, aliasInput.text ) + MMNetworkProviderDrawer { + id: networkProviderDrawer - providersModel.addProvider( aliasInput.text, deviceAddress, "external_ip" ) + onConfirmed: function( alias, deviceAddress ) { + PositionKit.positionProvider = PositionKit.constructProvider( "external_ip", deviceAddress, alias ) + providersModel.addProvider( alias, deviceAddress, "external_ip" ) + connectingDialogLoaderNetwork.open() } } MMComponents.MMMessage { + id: infoMessage + visible: !listview.visible width: parent.width anchors.centerIn: parent @@ -236,10 +168,10 @@ MMComponents.MMPage { image: __style.externalGpsRedImage title: qsTr( "Connecting to external receivers via bluetooth is not supported" ) description: qsTr( "This function is not available on iOS. " + - "Your hardware vendor may provide a custom " + - "app that connects to the receiver and sets position. " + - "The app will still think it is the internal GPS of " + - "your phone/tablet." ) + "Your hardware vendor may provide a custom " + + "app that connects to the receiver and sets position. " + + "The app will still think it is the internal GPS of " + + "your phone/tablet." ) link: __inputHelp.howToConnectGPSLink } @@ -252,7 +184,7 @@ MMComponents.MMPage { } onRemoveProvider: { - if (removeDialog.providerId === "") { + if ( removeDialog.providerId === "" ) { close() return } @@ -273,7 +205,23 @@ MMComponents.MMPage { id: bluetoothDiscoveryLoader active: false - sourceComponent: bluetoothDiscoveryDrawerComponent + source: Qt.resolvedUrl( "MMAddPositionProviderDrawer.qml" ) + + onLoaded: item.open() + } + + Connections { + target: bluetoothDiscoveryLoader.item + + function onInitiatedConnectionTo( deviceAddress, deviceName ) { + PositionKit.positionProvider = PositionKit.constructProvider( "external_bt", deviceAddress, deviceName ) + providersModel.addProvider( deviceName, deviceAddress, "external_bt" ) + bluetoothDiscoveryLoader.item.list.model.discovering = false + bluetoothDiscoveryLoader.item.close() + connectingDialogLoader.open() + } + + function onClosed() { bluetoothDiscoveryLoader.active = false } } Loader { @@ -281,44 +229,49 @@ MMComponents.MMPage { active: false asynchronous: true - sourceComponent: connectionToSavedProviderDialogComponent + source: Qt.resolvedUrl( "MMExternalProviderConnectionDrawer.qml" ) + + onLoaded: { + item.providerType = "bluetooth" + item.open() + } function open() { active = true focus = true } } - } - Component { - id: bluetoothDiscoveryDrawerComponent + Connections { + target: connectingDialogLoader.item + + function onClosed() { connectingDialogLoader.active = false } + function onFailure() { PositionKit.positionProvider = PositionKit.constructProvider( "internal", "devicegps", "" ) } + } - MMAddPositionProviderDrawer { - onInitiatedConnectionTo: function ( deviceAddress, deviceName ) { - __positionKit.positionProvider = __positionKit.constructProvider( "external_bt", deviceAddress, deviceName ) + Loader { + id: connectingDialogLoaderNetwork - providersModel.addProvider( deviceName, deviceAddress, "external_bt" ) - list.model.discovering = false - close() + active: false + asynchronous: true + source: Qt.resolvedUrl( "MMExternalProviderConnectionDrawer.qml" ) - connectingDialogLoader.open() + onLoaded: { + item.providerType = "network" + item.open() } - onClosed: bluetoothDiscoveryLoader.active = false - Component.onCompleted: open() + function open() { + active = true + focus = true + } } - } - Component { - id: connectionToSavedProviderDialogComponent + Connections { + target: connectingDialogLoaderNetwork.item - MMBluetoothConnectionDrawer { - onClosed: connectingDialogLoader.active = false - - // revert position provider back to internal provider - onFailure: PositionKit.positionProvider = PositionKit.constructProvider( "internal", "devicegps", "" ) - - Component.onCompleted: open() + function onClosed() { connectingDialogLoaderNetwork.active = false } + function onFailure() { PositionKit.positionProvider = PositionKit.constructProvider( "internal", "devicegps", "" ) } } } @@ -346,5 +299,8 @@ MMComponents.MMPage { if ( type === "external_bt" ) { connectingDialogLoader.open() } + else if ( type === "external_ip" ) { + connectingDialogLoaderNetwork.open() + } } } diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml new file mode 100644 index 000000000..af281397e --- /dev/null +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -0,0 +1,102 @@ +/*************************************************************************** + * * + * 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; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +import QtQuick + +import "../components" as MMComponents +import "../account/components" as MMAccountComponents + +MMComponents.MMListDrawer { + id: root + + property string selectedProviderType: "" + + signal bluetoothSelected() + signal networkSelected() + + drawerHeader.title: qsTr( "Connect new receiver" ) + drawerHeader.titleFont: __style.t2 + + onOpened: root.selectedProviderType = "" + + list.model: ListModel { + id: providerTypeModel + + Component.onCompleted: { + providerTypeModel.append( [ + { name: qsTr( "Bluetooth" ), description: qsTr( "Connect via Bluetooth" ), type: "bluetooth", icon: __style.bluetoothIcon }, + { name: qsTr( "Network" ), description: qsTr( "Connect via IP address" ), type: "network", icon: __style.networkIcon } + ] ) + } + } + + list.header: MMComponents.MMText { + width: ListView.view.width + + text: qsTr( "This function is not available on iOS. Your hardware vendor may provide a custom app that connects to the receiver and sets position. Mergin Maps will still think it is the internal GPS of your phone/tablet." ) + + font: __style.p5 + color: __style.nightColor + + wrapMode: Text.Wrap + bottomPadding: __style.margin16 + } + + list.delegate: Item { + width: ListView.view.width + height: checkbox.height + __style.margin12 + + MMAccountComponents.MMIconCheckBoxHorizontal { + id: checkbox + + width: parent.width + showBorder: true + sourceIcon: model.icon + text: model.name + description: model.description + checked: root.selectedProviderType === model.type + + onClicked: { + if ( root.selectedProviderType === model.type ) { + root.selectedProviderType = "" + } + else { + root.selectedProviderType = model.type + } + } + } + } + + list.footer: Item { + width: ListView.view.width + height: continueButton.height + __style.margin20 + + MMComponents.MMButton { + id: continueButton + + width: parent.width + anchors.top: parent.top + anchors.topMargin: __style.margin8 + + text: qsTr( "Continue" ) + enabled: root.selectedProviderType !== "" + + onClicked: { + root.close() + + if ( root.selectedProviderType === "bluetooth" ) { + root.bluetoothSelected() + } + else if ( root.selectedProviderType === "network" ) { + root.networkSelected() + } + } + } + } +} diff --git a/gallery/qml.qrc b/gallery/qml.qrc index 9d82ad2d8..a8a055cbe 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -140,7 +140,7 @@ ../app/qml/settings/components/MMSettingsInput.qml ../app/qml/settings/components/MMSettingsDropdown.qml ../app/qml/settings/components/MMSettingsSwitch.qml - ../app/qml/gps/MMBluetoothConnectionDrawer.qml + ../app/qml/gps/MMExternalProviderConnectionDrawer.qml ../app/qml/gps/MMGpsDataDrawer.qml ../app/qml/gps/components/MMGpsDataText.qml ../app/qml/dialogs/MMSyncFailedDialog.qml diff --git a/gallery/qml/pages/ChecksPage.qml b/gallery/qml/pages/ChecksPage.qml index 9e097a4b1..11afacbb4 100644 --- a/gallery/qml/pages/ChecksPage.qml +++ b/gallery/qml/pages/ChecksPage.qml @@ -90,9 +90,11 @@ Column { MMAccountComponents.MMIconCheckBoxHorizontal { checked: true + width: 300 * __dp sourceIcon: __style.redditIcon text: "Reddit" - small: true + description: "This is a small description to check the functionality of this component" + small: false } } } diff --git a/gallery/qml/pages/DrawerPage.qml b/gallery/qml/pages/DrawerPage.qml index e3400a423..47a0aab1f 100644 --- a/gallery/qml/pages/DrawerPage.qml +++ b/gallery/qml/pages/DrawerPage.qml @@ -59,7 +59,7 @@ Page { } Button { - text: "MMBluetoothConnectionDrawer" + text: "MMExternalProviderConnectionDrawer" onClicked: { bluetoothConnectionDrawer.positionProvider.state = PositionProvider.Connecting bluetoothConnectionTimer.start() @@ -197,8 +197,9 @@ Page { } } - MMBluetoothConnectionDrawer { + MMExternalProviderConnectionDrawer { id: bluetoothConnectionDrawer + providerType: "bluetooth" howToConnectGPSLink: "www.merginmaps.com" positionProvider: QtObject { diff --git a/gallery/qml/pages/ImagesPage.qml b/gallery/qml/pages/ImagesPage.qml index ed931d11b..59bd68133 100644 --- a/gallery/qml/pages/ImagesPage.qml +++ b/gallery/qml/pages/ImagesPage.qml @@ -38,7 +38,8 @@ ScrollView { Column { Image { source: __style.positionTrackingRunningImage } Text { text: "positionTrackingRunningImage" } } Column { Image { source: __style.noMapThemesImage } Text { text: "noMapThemesImage" } } Column { Image { source: __style.syncImage } Text { text: "syncImage" } } - Column { Image { source: __style.externalGpsGreenImage } Text { text: "externalGpsGreenImage" } } + Column { Image { source: __style.externalBluetoothGreenImage } Text { text: "externalBluetoothGreenImage" } } + Column { Image { source: __style.externalNetworkGreenImage } Text { text: "externalNetworkGreenImage" } } Column { Image { source: __style.externalGpsRedImage } Text { text: "externalGpsRedImage" } } Column { Image { source: __style.reachedDataLimitImage } Text { text: "reachedDataLimitImage" } } Column { Image { source: __style.positiveMMSymbolImage } Text { text: "positiveMMSymbolImage" } } From 66e9bffef86847be9392ca25616f4f1ad8ffc82d Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Thu, 12 Mar 2026 15:35:00 +0200 Subject: [PATCH 17/30] Fixed rebase conflicts --- app/position/positionkit.cpp | 8 ++++---- app/position/providers/bluetoothpositionprovider.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index ee55ed6f1..19685c80a 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -153,7 +153,7 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c // type == internal if ( id == QStringLiteral( "simulated" ) ) { - AbstractPositionProvider *provider = new SimulatedPositionProvider(); + AbstractPositionProvider *provider = new SimulatedPositionProvider( *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } @@ -167,12 +167,12 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c // TODO: inform user + use AndroidPositionProvider::fusedErrorString() output? // fallback to the default - at this point the Qt Positioning implementation - AbstractPositionProvider *provider = new InternalPositionProvider(); + AbstractPositionProvider *provider = new InternalPositionProvider( *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } __android_log_print( ANDROID_LOG_INFO, "CPP", "MAKE PROVIDER %d", fused ); - AbstractPositionProvider *provider = new AndroidPositionProvider( fused ); + AbstractPositionProvider *provider = new AndroidPositionProvider( fused, *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } @@ -180,7 +180,7 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c // id == devicegps { - AbstractPositionProvider *provider = new InternalPositionProvider(); + AbstractPositionProvider *provider = new InternalPositionProvider( *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } diff --git a/app/position/providers/bluetoothpositionprovider.h b/app/position/providers/bluetoothpositionprovider.h index ad60d19a9..8797c8ec1 100644 --- a/app/position/providers/bluetoothpositionprovider.h +++ b/app/position/providers/bluetoothpositionprovider.h @@ -34,7 +34,7 @@ class BluetoothPositionProvider : public AbstractPositionProvider }; public: - BluetoothPositionProvider( const QString &addr, const QString &name, QObject *parent = nullptr ); + BluetoothPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); ~BluetoothPositionProvider() override; void startUpdates() override; From 314f3216bf25c2738a39fb8d3bb8c3124d302676 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Mon, 16 Mar 2026 16:20:39 +0200 Subject: [PATCH 18/30] Modified message Added ip address getter for network provider Changed provider constructor to make iOS device connect to external IP provider --- app/position/positionkit.cpp | 8 +-- .../providers/networkpositionprovider.cpp | 15 +++- .../providers/networkpositionprovider.h | 1 + .../MMExternalProviderConnectionDrawer.qml | 68 ++++++++++++------- 4 files changed, 60 insertions(+), 32 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 19685c80a..509144c80 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -128,18 +128,14 @@ QString PositionKit::positionProviderName() const AbstractPositionProvider *PositionKit::constructProvider( const QString &type, const QString &id, const QString &name ) { QString providerType( type ); - - // set internal provider for platforms that do not support reading bluetooth serial -#ifndef HAVE_BLUETOOTH - providerType = QStringLiteral( "internal" ); -#endif - if ( providerType == QStringLiteral( "external_bt" ) ) { #ifdef HAVE_BLUETOOTH AbstractPositionProvider *provider = new BluetoothPositionProvider( id, name, *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; +#else + providerType = QStringLiteral( "internal" ); #endif } diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index d7abb3627..e7363d6ea 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -125,11 +125,17 @@ void NetworkPositionProvider::positionUpdateReceived() mUdpReconnectTimer.stop(); } + const QByteArray rawNmeaData = socket->readAll(); + + if ( rawNmeaData.isEmpty() || !rawNmeaData.contains( '$' ) ) + { + return; + } + // if by any chance we showed wrong message in the status like "no connection", fix it here // we know the connection is working because we just received data from the device setState( tr( "Connected" ), State::Connected ); - const QByteArray rawNmeaData = socket->readAll(); const QString nmeaData( rawNmeaData ); const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); GeoPosition transformedPosition = mPositionTransformer->processNetworkPosition( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); @@ -184,6 +190,11 @@ void NetworkPositionProvider::reconnectTimeout() } } +QString NetworkPositionProvider::getIpAddress() const +{ + return mTargetAddress; +} + void NetworkPositionProvider::reconnect() { mReconnectTimer.stop(); @@ -191,6 +202,8 @@ void NetworkPositionProvider::reconnect() setState( tr( "Reconnecting" ), State::Connecting ); stopUpdates(); + mReconnectTimer.stop(); + startUpdates(); } diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index 9d4bc080f..3c87ddbbf 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -37,6 +37,7 @@ class NetworkPositionProvider : public AbstractPositionProvider void startUpdates() override; void stopUpdates() override; void closeProvider() override; + Q_INVOKABLE QString getIpAddress() const; // returns the IP address we try to connect/ are connected to public slots: // processes the received nmea data and emits new position diff --git a/app/qml/gps/MMExternalProviderConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml index 92e113bfc..f3f0b9041 100644 --- a/app/qml/gps/MMExternalProviderConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -24,27 +24,44 @@ MMComponents.MMDrawer { property string howToConnectGPSLink: __inputHelp.howToConnectGPSLink property string titleText: { - if ( rootstate.state === "working" ) + if ( root.providerType === "network" ) { - if ( !root.positionProvider ) return "" - if ( root.positionProvider.name() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.name() + connectingSuffixAnimation - return qsTr( "Connecting" ) + connectingSuffixAnimation - } - else if ( rootstate.state === "success" ) - { - return qsTr( "Connected" ) + if ( rootstate.state === "working" ) + { + if ( !root.positionProvider ) return "" + if ( root.positionProvider.getIpAddress() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.getIpAddress() + connectingSuffixAnimation + return qsTr( "Connecting" ) + connectingSuffixAnimation + } + else if ( rootstate.state === "success" ) + { + return qsTr( "Connected" ) + } + else if ( rootstate.state === "fail" ) + { + return qsTr( "Failed to connect to" ) + " " + ( root.positionProvider ? root.positionProvider.getIpAddress() : "" ) + } + else return qsTr( "We were not able to connect to the specified IP address. Please try again later." ) } - else if ( rootstate.state === "fail") + else if ( root.providerType === "bluetooth" ) { - return qsTr( "Failed to connect to" ) + " " + ( root.positionProvider ? root.positionProvider.name() : "" ) - } - else - { - if( root.providerType === "bluetooth") - return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to.") - else if (root.providerType === "network") - // either NoConnection or WaitingToReconnect - return qsTr( "We were not able to connect to the specified IP address. Please try again later." ) + if ( rootstate.state === "working" ) + { + if ( !root.positionProvider ) return "" + if ( root.positionProvider.name() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.name() + connectingSuffixAnimation + return qsTr( "Connecting" ) + connectingSuffixAnimation + } + else if ( rootstate.state === "success" ) + { + return qsTr( "Connected" ) + } + else if ( rootstate.state === "fail" ) + { + return qsTr( "Failed to connect to" ) + " " + ( root.positionProvider ? root.positionProvider.name() : "" ) + } + else + { + if ( root.providerType === "bluetooth" ) return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) + } } } @@ -53,9 +70,9 @@ MMComponents.MMDrawer { property string descriptionText: { if ( rootstate.state === "working" ) { - if( root.providerType === "bluetooth") + if ( root.providerType === "bluetooth" ) return qsTr( "You might be asked to pair your device during this process." ) - else if( root.providerType === "network") + else if ( root.providerType === "network" ) return qsTr( "This might take a while..." ) } else if ( rootstate.state === "success" ) @@ -70,9 +87,9 @@ MMComponents.MMDrawer { else { - if( root.providerType === "bluetooth") + if ( root.providerType === "bluetooth" ) return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) - else if( root.providerType === "network") + else if ( root.providerType === "network" ) return qsTr( "We were not able to connect to the specified IP address. Please try again later." ) } } @@ -82,10 +99,11 @@ MMComponents.MMDrawer { { return __style.externalGpsRedImage } - else { - if( root.providerType === "bluetooth" ) + else + { + if ( root.providerType === "bluetooth" ) return __style.externalBluetoothGreenImage - else if (root.providerType === "network") + else if ( root.providerType === "network" ) return __style.externalNetworkGreenImage } } From db4dfae437270f634a5546c26f7c01d0a008ca65 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Thu, 26 Mar 2026 14:52:52 +0200 Subject: [PATCH 19/30] Updated text descriptions Added hyperlink Modified the getIPAddress method Enhanced visuals --- .../providers/networkpositionprovider.cpp | 2 +- .../providers/networkpositionprovider.h | 2 +- app/qml/gps/MMAddPositionProviderDrawer.qml | 49 ++++++++++++------- .../MMExternalProviderConnectionDrawer.qml | 10 ++-- app/qml/gps/MMNetworkProviderDrawer.qml | 11 +++-- app/qml/gps/MMProviderTypeDrawer.qml | 36 +++++++++----- 6 files changed, 70 insertions(+), 40 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index e7363d6ea..983b8a679 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -192,7 +192,7 @@ void NetworkPositionProvider::reconnectTimeout() QString NetworkPositionProvider::getIpAddress() const { - return mTargetAddress; + return mTargetAddress + ":" + QString::number( mTargetPort ); } void NetworkPositionProvider::reconnect() diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index 3c87ddbbf..e87c1f36a 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -37,7 +37,7 @@ class NetworkPositionProvider : public AbstractPositionProvider void startUpdates() override; void stopUpdates() override; void closeProvider() override; - Q_INVOKABLE QString getIpAddress() const; // returns the IP address we try to connect/ are connected to + Q_INVOKABLE QString getIpAddress() const; // returns the IP address and the port we try to connect/ are connected to public slots: // processes the received nmea data and emits new position diff --git a/app/qml/gps/MMAddPositionProviderDrawer.qml b/app/qml/gps/MMAddPositionProviderDrawer.qml index 5531a52ca..65ccf97f1 100644 --- a/app/qml/gps/MMAddPositionProviderDrawer.qml +++ b/app/qml/gps/MMAddPositionProviderDrawer.qml @@ -21,7 +21,9 @@ MMComponents.MMListDrawer { signal initiatedConnectionTo( string deviceAddress, string deviceName ) showFullScreen: true - drawerHeader.title: qsTr( "Connect to bluetooth device" ) + drawerHeader.title: qsTr( "Bluetooth Connection" ) + + list.height: root.drawerContentAvailableHeight list.model: MM.BluetoothDiscoveryModel { id: btModel @@ -97,36 +99,49 @@ MMComponents.MMListDrawer { } } + // footer Component { id: discoveringMessageComponent - Column { + Item { width: ListView.view.width + height: Math.max( contentColumn.implicitHeight, + ListView.view.height - ( ListView.view.contentHeight - height ) ) - spacing: __style.spacing16 - - MMComponents.MMListSpacer { height: __style.margin40 } + Column { + id: contentColumn - Image { + anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter - source: __style.mmSymbolImage + width: parent.width + spacing: __style.spacing16 - width: 32 * __dp - height: 32 * __dp + Image { + anchors.horizontalCenter: parent.horizontalCenter - fillMode: Image.PreserveAspectFit - } + source: __style.mmSymbolImage - MMComponents.MMText { - width: parent.width + width: 32 * __dp + height: 32 * __dp - text: qsTr( "Looking for more devices" ) + "..." + fillMode: Image.PreserveAspectFit + } - font: __style.t3 - color: __style.forestColor + MMComponents.MMListSpacer { height: __style.margin20 } + + MMComponents.MMText { + width: parent.width + + text: qsTr( "Scanning for devices" ) + "..." + + font: __style.t3 + color: __style.forestColor + + horizontalAlignment: Text.AlignHCenter + } - horizontalAlignment: Text.AlignHCenter + MMComponents.MMListSpacer { height: __style.margin20 } } } } diff --git a/app/qml/gps/MMExternalProviderConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml index f3f0b9041..eec55a508 100644 --- a/app/qml/gps/MMExternalProviderConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -29,8 +29,7 @@ MMComponents.MMDrawer { if ( rootstate.state === "working" ) { if ( !root.positionProvider ) return "" - if ( root.positionProvider.getIpAddress() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.getIpAddress() + connectingSuffixAnimation - return qsTr( "Connecting" ) + connectingSuffixAnimation + return qsTr( "Connecting to external receiver" ) } else if ( rootstate.state === "success" ) { @@ -47,7 +46,7 @@ MMComponents.MMDrawer { if ( rootstate.state === "working" ) { if ( !root.positionProvider ) return "" - if ( root.positionProvider.name() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.name() + connectingSuffixAnimation + if ( root.positionProvider.name() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.name() return qsTr( "Connecting" ) + connectingSuffixAnimation } else if ( rootstate.state === "success" ) @@ -73,7 +72,8 @@ MMComponents.MMDrawer { if ( root.providerType === "bluetooth" ) return qsTr( "You might be asked to pair your device during this process." ) else if ( root.providerType === "network" ) - return qsTr( "This might take a while..." ) + if ( root.positionProvider.getIpAddress() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.getIpAddress() + ". You can close this panel, the app will continue in the background." + return qsTr( "Connecting" ) + connectingSuffixAnimation } else if ( rootstate.state === "success" ) { @@ -136,7 +136,7 @@ MMComponents.MMDrawer { state: "working" } - drawerBottomMargin: __style.margin20 + drawerBottomMargin: __style.margin40 drawerContent: MMComponents.MMScrollView { width: parent.width diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml index d5632fdec..9e23c56a6 100644 --- a/app/qml/gps/MMNetworkProviderDrawer.qml +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -26,14 +26,17 @@ MMComponents.MMDrawer { MMComponents.MMText { width: parent.width - - text: qsTr( "External receivers can be connected via network in iOS devices. Some of the known devices that support network connection (TCP or UDP) are EOS and Emlid." ) - + + text: qsTr( "Enter the IP address and port for your receiver. You can find these in your manufacturer's app. TCP and UDP connections are supported. Optionally, add a nickname to tell your receivers apart." ) font: __style.p5 color: __style.nightColor wrapMode: Text.Wrap horizontalAlignment: Text.AlignJustify + + onLinkActivated: function( link ) { + Qt.openUrlExternally( link ) + } } MMInputs.MMTextInput { @@ -42,7 +45,7 @@ MMComponents.MMDrawer { width: parent.width textFieldBackground.color: __style.lightGreenColor - title: qsTr( "IP address" ) + title: qsTr( "IP Address" ) placeholderText: qsTr( "localhost" ) onTextEdited: errorMsg = "" diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index af281397e..2a5527609 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -30,23 +30,35 @@ MMComponents.MMListDrawer { Component.onCompleted: { providerTypeModel.append( [ - { name: qsTr( "Bluetooth" ), description: qsTr( "Connect via Bluetooth" ), type: "bluetooth", icon: __style.bluetoothIcon }, - { name: qsTr( "Network" ), description: qsTr( "Connect via IP address" ), type: "network", icon: __style.networkIcon } + { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon }, + { name: qsTr( "Network" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } ] ) } } list.header: MMComponents.MMText { - width: ListView.view.width - - text: qsTr( "This function is not available on iOS. Your hardware vendor may provide a custom app that connects to the receiver and sets position. Mergin Maps will still think it is the internal GPS of your phone/tablet." ) - - font: __style.p5 - color: __style.nightColor - - wrapMode: Text.Wrap - bottomPadding: __style.margin16 - } + width: ListView.view.width + + text: __inputUtils.htmlLink( + qsTr( "External receivers use different connection methods depending on the manufacturer. Select a connection type below, or %1check our documentation%2 for supported devices and setup instructions." ), + __style.nightColor, + __inputHelp.howToConnectGPSLink, + "", + true, + false + ) + font: __style.p5 + color: __style.nightColor + textFormat: Text.RichText + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignJustify + bottomPadding: __style.margin16 + + onLinkActivated: function( link ) { + Qt.openUrlExternally( link ) + } + } list.delegate: Item { width: ListView.view.width From d29547887779058a724cb2cc77184339d52f2f22 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Thu, 2 Apr 2026 16:25:05 +0300 Subject: [PATCH 20/30] Modified the external device description in position provider drawer fIxed gps data button alignment Corrected some QML warnings for the position kit --- app/qml/gps/MMGpsDataDrawer.qml | 418 +++++++++++++------------ app/qml/gps/MMPositionProviderPage.qml | 15 +- app/qml/map/MMMapController.qml | 4 +- 3 files changed, 232 insertions(+), 205 deletions(-) diff --git a/app/qml/gps/MMGpsDataDrawer.qml b/app/qml/gps/MMGpsDataDrawer.qml index ad4456e8c..3265edc86 100644 --- a/app/qml/gps/MMGpsDataDrawer.qml +++ b/app/qml/gps/MMGpsDataDrawer.qml @@ -22,328 +22,344 @@ MMComponents.MMDrawer { property var mapSettings property bool showReceiversButton: true + readonly property real reservedButtonSpace: __style.spacing20 + manageButton.implicitHeight + __style.safeAreaBottom + __style.margin8 signal manageReceiversClicked() drawerHeader.title: qsTr( "GPS info" ) + drawerBottomMargin: 0 - drawerContent: MMComponents.MMScrollView { + drawerContent: Item { width: parent.width - height: root.maxHeightHit ? root.drawerContentAvailableHeight : contentHeight + height: root.maxHeightHit ? root.drawerContentAvailableHeight + : ( scrollView.contentHeight + reservedButtonSpace ) - Column { - width: parent.width + MMComponents.MMScrollView { + id: scrollView - spacing: __style.spacing40 + width: parent.width + height: root.maxHeightHit ? ( parent.height - reservedButtonSpace) + : contentHeight - Item { + Column { width: parent.width - height: datagrid.implicitHeight - Rectangle { - // horizontal line after each row in the grid - anchors { - fill: datagrid - bottomMargin: datagrid.children[0].height + Item { + width: parent.width + height: datagrid.implicitHeight + + Rectangle { + // horizontal line after each row in the grid + anchors { + fill: datagrid + bottomMargin: datagrid.children[0].height + } + color: __style.greyColor } - color: __style.greyColor - } - Grid { - id: datagrid + Grid { + id: datagrid - width: parent.width - columns: 2 + width: parent.width + columns: 2 - rowSpacing: __style.margin1 + rowSpacing: __style.margin1 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - title: qsTr( "Source" ) - value: PositionKit.positionProvider ? PositionKit.positionProviderName : qsTr( "No receiver" ) + title: qsTr( "Source" ) + value: PositionKit.positionProvider ? PositionKit.positionProviderName : qsTr( "No receiver" ) - alignmentRight: Positioner.index % 2 === 1 - } + alignmentRight: Positioner.index % 2 === 1 + } - MMGpsComponents.MMGpsDataText { - id: statusCell + MMGpsComponents.MMGpsDataText { + id: statusCell - width: parent.width / 2 + width: parent.width / 2 - visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") + visible: PositionKit.positionProvider && PositionKit.positionProvider.type().includes("external") - title: qsTr( "Status" ) - value: PositionKit.positionProvider ? PositionKit.positionProvider.stateMessage : "" + title: qsTr( "Status" ) + value: PositionKit.positionProvider ? PositionKit.positionProvider.stateMessage : "" - alignmentRight: Positioner.index % 2 === 1 - } + alignmentRight: Positioner.index % 2 === 1 + } - Rectangle { // placeholder cell to keep long/lat in one line - width: parent.width - height: statusCell.height + Rectangle { // placeholder cell to keep long/lat in one line + width: parent.width + height: statusCell.height - color: __style.polarColor - visible: !statusCell.visible - } + color: __style.polarColor + visible: !statusCell.visible + } + + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 + + title: qsTr( "Longitude") + value: { + if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.longitude ) ) { + return qsTr( "N/A" ) + } - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + let coordParts = internal.coordinatesInDegrees.split(", ") + if ( coordParts.length > 1 ) + return coordParts[1] - title: qsTr( "Longitude") - value: { - if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.longitude ) ) { return qsTr( "N/A" ) } - let coordParts = internal.coordinatesInDegrees.split(", ") - if ( coordParts.length > 1 ) - return coordParts[1] - - return qsTr( "N/A" ) + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 + + title: qsTr( "Latitude" ) + value: { + if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.latitude ) ) { + return qsTr( "N/A" ) + } - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + let coordParts = internal.coordinatesInDegrees.split(", ") + if ( coordParts.length > 1 ) + return coordParts[0] - title: qsTr( "Latitude" ) - value: { - if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.latitude ) ) { return qsTr( "N/A" ) } - let coordParts = internal.coordinatesInDegrees.split(", ") - if ( coordParts.length > 1 ) - return coordParts[0] - - return qsTr( "N/A" ) + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + title: qsTr( "X" ) + value: { + if ( !PositionKit.hasPosition || Number.isNaN( mapPositioning.mapPosition.x ) ) { + return qsTr( "N/A" ) + } - title: qsTr( "X" ) - value: { - if ( !PositionKit.hasPosition || Number.isNaN( mapPositioning.mapPosition.x ) ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( mapPositioning.mapPosition.x, 2 ) } - __inputUtils.formatNumber( mapPositioning.mapPosition.x, 2 ) + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + title: qsTr( "Y" ) + value: { + if ( !PositionKit.hasPosition || Number.isNaN( mapPositioning.mapPosition.y ) ) { + return qsTr( "N/A" ) + } - title: qsTr( "Y" ) - value: { - if ( !PositionKit.hasPosition || Number.isNaN( mapPositioning.mapPosition.y ) ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( mapPositioning.mapPosition.y, 2 ) } - __inputUtils.formatNumber( mapPositioning.mapPosition.y, 2 ) + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + title: qsTr( "Horizontal accuracy" ) + value: { + if ( !PositionKit.hasPosition || PositionKit.horizontalAccuracy < 0 ) { + return qsTr( "N/A" ) + } - title: qsTr( "Horizontal accuracy" ) - value: { - if ( !PositionKit.hasPosition || PositionKit.horizontalAccuracy < 0 ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( PositionKit.horizontalAccuracy, 2 ) + " m" } - __inputUtils.formatNumber( PositionKit.horizontalAccuracy, 2 ) + " m" + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + title: qsTr( "Vertical accuracy" ) + value: { + if ( !PositionKit.hasPosition || PositionKit.verticalAccuracy < 0 ) { + return qsTr( "N/A" ) + } - title: qsTr( "Vertical accuracy" ) - value: { - if ( !PositionKit.hasPosition || PositionKit.verticalAccuracy < 0 ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( PositionKit.verticalAccuracy, 2 ) + " m" } - __inputUtils.formatNumber( PositionKit.verticalAccuracy, 2 ) + " m" + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } - - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - title: qsTr( "Altitude" ) - value: { - if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.altitude ) ) { - return qsTr( "N/A" ) + title: qsTr( "Altitude" ) + value: { + if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.altitude ) ) { + return qsTr( "N/A" ) + } + __inputUtils.formatNumber( PositionKit.altitude, 2 ) + " m" } - __inputUtils.formatNumber( PositionKit.altitude, 2 ) + " m" + + alignmentRight: Positioner.index % 2 === 1 + desc: PositionKit.positionCrs3DGeoidModelName().length > 0 ? qsTr("Orthometric height, using %1 geoid").arg(PositionKit.positionCrs3DGeoidModelName()) : qsTr("Elevation using unspecified grid") } - alignmentRight: Positioner.index % 2 === 1 - desc: PositionKit.positionCrs3DGeoidModelName().length > 0 ? qsTr("Orthometric height, using %1 geoid").arg(PositionKit.positionCrs3DGeoidModelName()) : qsTr("Elevation using unspecified grid") - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + title: qsTr( "Fix quality" ) + value: { + if ( !PositionKit.hasPosition ) { + return qsTr( "N/A" ) + } - title: qsTr( "Fix quality" ) - value: { - if ( !PositionKit.hasPosition ) { - return qsTr( "N/A" ) + PositionKit.fix } - PositionKit.fix + visible: PositionKit.positionProvider && PositionKit.positionProvider.type().includes("external") + + alignmentRight: Positioner.index % 2 === 1 } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - alignmentRight: Positioner.index % 2 === 1 - } + title: qsTr( "Satellites (in use/view)" ) + value: { + if ( PositionKit.satellitesUsed < 0 || PositionKit.satellitesVisible < 0 ) + { + return qsTr( "N/A" ) + } - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 - - title: qsTr( "Satellites (in use/view)" ) - value: { - if ( PositionKit.satellitesUsed < 0 || PositionKit.satellitesVisible < 0 ) - { - return qsTr( "N/A" ) + PositionKit.satellitesUsed + "/" + PositionKit.satellitesVisible } - PositionKit.satellitesUsed + "/" + PositionKit.satellitesVisible + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + title: qsTr( "HDOP" ) + value: { + if ( !PositionKit.hasPosition || PositionKit.hdop < 0 ) { + return qsTr( "N/A" ) + } - title: qsTr( "HDOP" ) - value: { - if ( !PositionKit.hasPosition || PositionKit.hdop < 0 ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( PositionKit.hdop, 2 ) } - __inputUtils.formatNumber( PositionKit.hdop, 2 ) + visible: PositionKit.positionProvider && PositionKit.positionProvider.type().includes("external") + + alignmentRight: Positioner.index % 2 === 1 } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - alignmentRight: Positioner.index % 2 === 1 - } + title: qsTr( "VDOP" ) + value: { + if ( !PositionKit.hasPosition || PositionKit.vdop < 0 ) { + return qsTr( "N/A" ) + } - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 - - title: qsTr( "VDOP" ) - value: { - if ( !PositionKit.hasPosition || PositionKit.vdop < 0 ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( PositionKit.vdop, 2 ) } - __inputUtils.formatNumber( PositionKit.vdop, 2 ) - } + visible: PositionKit.positionProvider && PositionKit.positionProvider.type().includes("external") - visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") + alignmentRight: Positioner.index % 2 === 1 + } - alignmentRight: Positioner.index % 2 === 1 - } + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + title: qsTr( "PDOP" ) + value: { + if ( !PositionKit.hasPosition || PositionKit.pdop < 0 ) { + return qsTr( "N/A" ) + } - title: qsTr( "PDOP" ) - value: { - if ( !PositionKit.hasPosition || PositionKit.pdop < 0 ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( PositionKit.pdop, 2 ) } - __inputUtils.formatNumber( PositionKit.pdop, 2 ) + visible: PositionKit.positionProvider && PositionKit.positionProvider.type().includes("external") + + alignmentRight: Positioner.index % 2 === 1 } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - alignmentRight: Positioner.index % 2 === 1 - } + title: qsTr( "Speed" ) + value: { + if ( !PositionKit.hasPosition || PositionKit.speed < 0 ) { + return qsTr( "N/A" ) + } - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 - - title: qsTr( "Speed" ) - value: { - if ( !PositionKit.hasPosition || PositionKit.speed < 0 ) { - return qsTr( "N/A" ) + __inputUtils.formatNumber( PositionKit.speed, 2 ) + " km/h" } - __inputUtils.formatNumber( PositionKit.speed, 2 ) + " km/h" + alignmentRight: Positioner.index % 2 === 1 } - alignmentRight: Positioner.index % 2 === 1 - } - - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - title: qsTr( "Last Fix" ) - value: PositionKit.lastRead.toLocaleTimeString( Qt.locale() ) || qsTr( "N/A" ) + title: qsTr( "Last Fix" ) + value: PositionKit.lastRead.toLocaleTimeString( Qt.locale() ) || qsTr( "N/A" ) - alignmentRight: Positioner.index % 2 === 1 - } + alignmentRight: Positioner.index % 2 === 1 + } - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - title: qsTr( "GPS antenna height" ) - value: AppSettings.gpsAntennaHeight > 0 ? __inputUtils.formatNumber(AppSettings.gpsAntennaHeight, 3) + " m" : qsTr( "Not set" ) + title: qsTr( "GPS antenna height" ) + value: AppSettings.gpsAntennaHeight > 0 ? __inputUtils.formatNumber(AppSettings.gpsAntennaHeight, 3) + " m" : qsTr( "Not set" ) - alignmentRight: Positioner.index % 2 === 1 - } + alignmentRight: Positioner.index % 2 === 1 + } - MMGpsComponents.MMGpsDataText { - width: parent.width / 2 + MMGpsComponents.MMGpsDataText { + width: parent.width / 2 - title: qsTr( "Geoid separation" ) - value: { - if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.geoidSeparation ) ) { - return qsTr( "N/A" ) + title: qsTr( "Geoid separation" ) + value: { + if ( !PositionKit.hasPosition || Number.isNaN( PositionKit.geoidSeparation ) ) { + return qsTr( "N/A" ) + } + __inputUtils.formatNumber( PositionKit.geoidSeparation, 2 ) + " m" } - __inputUtils.formatNumber( PositionKit.geoidSeparation, 2 ) + " m" - } - visible: __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") + visible: PositionKit.positionProvider && PositionKit.positionProvider.type().includes("external") - alignmentRight: Positioner.index % 2 === 1 + alignmentRight: Positioner.index % 2 === 1 + } } } } + } - MMComponents.MMButton { - width: parent.width + MMComponents.MMButton { + id: manageButton + + width: parent.width - visible: root.showReceiversButton + visible: root.showReceiversButton - text: qsTr( "Manage GPS receivers" ) - onClicked: root.manageReceiversClicked() + anchors { + bottom: parent.bottom + bottomMargin: __style.safeAreaBottom + __style.margin8 } + + text: qsTr( "Manage GPS receivers" ) + onClicked: root.manageReceiversClicked() } } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 471e48ba6..c808939aa 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -76,8 +76,19 @@ MMComponents.MMPage { } } - text: model.ProviderName ? model.ProviderName : qsTr( "Unknown device" ) - secondaryText: listdelegate.isActive ? PositionKit.positionProvider.stateMessage : model.ProviderDescription + text: { + if ( model.ProviderName ) return model.ProviderName + if ( model.ProviderType === "external_ip" ) return qsTr( "Network device" ) + return qsTr( "Unknown device" ) + } + secondaryText: { + if ( listdelegate.isActive ) { + if ( model.ProviderType === "external_ip" ) + return PositionKit.positionProvider.stateMessage + " - " + PositionKit.positionProvider.getIpAddress() + return PositionKit.positionProvider.stateMessage + } + return model.ProviderDescription + } rightContent: MMComponents.MMRoundButton { visible: model.ProviderType !== "internal" diff --git a/app/qml/map/MMMapController.qml b/app/qml/map/MMMapController.qml index 6766d3f32..a9d5f8be2 100644 --- a/app/qml/map/MMMapController.qml +++ b/app/qml/map/MMMapController.qml @@ -679,7 +679,7 @@ Item { visible: { if ( root.mapExtentOffset > 0 && root.state !== "stakeout" ) return false - if ( __positionKit.positionProvider && __positionKit.positionProvider.type().includes("external") ) { + if ( PositionKit.positionProvider && PositionKit.positionProvider.type().includes("external") ) { // for external receivers we want to show gps panel and accuracy button // even when the GPS receiver is not sending position data return true @@ -699,7 +699,7 @@ Item { { return "" } - else if ( __positionKit.positionProvider.type().includes("external") ) + else if ( PositionKit.positionProvider.type().includes("external") ) { if ( PositionKit.positionProvider.state === MM.PositionProvider.Connecting ) { From 3eb777d7dc5cc1311829cb35616b47716fe6cae0 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Apr 2026 14:11:40 +0200 Subject: [PATCH 21/30] Fix minor UI things --- app/qml/gps/MMNetworkProviderDrawer.qml | 13 +++++++++++-- app/qml/gps/MMProviderTypeDrawer.qml | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml index 9e23c56a6..943a962d7 100644 --- a/app/qml/gps/MMNetworkProviderDrawer.qml +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -72,7 +72,7 @@ MMComponents.MMDrawer { textFieldBackground.color: __style.lightGreenColor title: qsTr( "Receiver nickname (optional)" ) - placeholderText: qsTr( "Green device" ) + placeholderText: qsTr( "External receiver" ) } MMComponents.MMButton { @@ -114,9 +114,18 @@ MMComponents.MMDrawer { const deviceAddress = ip + ":" + port - root.confirmed( aliasInput.text, deviceAddress ) + root.confirmed( aliasInput.text.trim(), deviceAddress ) + ipAddressInput.textField.clear() + portInput.textField.clear() + aliasInput.textField.clear() root.close() } } } + + onClosed: { + ipAddressInput.textField.clear() + portInput.textField.clear() + aliasInput.textField.clear() + } } diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index 2a5527609..60137d62f 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -31,7 +31,7 @@ MMComponents.MMListDrawer { Component.onCompleted: { providerTypeModel.append( [ { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon }, - { name: qsTr( "Network" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } + { name: qsTr( "Network (TCP, UDP)" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } ] ) } } From 005f0558ddbefdea47222329dcf5a2ee1bbf6131 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Apr 2026 15:36:27 +0200 Subject: [PATCH 22/30] Fix cpp check --- app/position/providers/networkpositionprovider.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 983b8a679..2ed7d9950 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -71,10 +71,16 @@ void NetworkPositionProvider::stopUpdates() void NetworkPositionProvider::closeProvider() { - mTcpSocket->close(); - mUdpSocket->close(); - if ( mTcpSocket ) mTcpSocket->disconnect(); - if ( mUdpSocket ) mUdpSocket->disconnect(); + if ( mTcpSocket ) + { + mTcpSocket->close(); + mTcpSocket->disconnect(); + } + if ( mUdpSocket ) + { + mUdpSocket->close(); + mUdpSocket->disconnect(); + } mUdpReconnectTimer.stop(); mReconnectTimer.stop(); From 23685570f9d952141886a4c12efe7f269b5c3255 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Tue, 28 Apr 2026 17:52:48 +0200 Subject: [PATCH 23/30] Add unique default names for network position providers --- app/inpututils.cpp | 23 ++++++++++++++++++++ app/inpututils.h | 13 +++++++++++ app/position/positionkit.cpp | 12 +++++++++- app/qml/gps/MMNetworkProviderDrawer.qml | 2 +- app/qml/gps/MMPositionProviderPage.qml | 3 +-- app/test/testutilsfunctions.cpp | 29 +++++++++++++++++++++++++ app/test/testutilsfunctions.h | 1 + 7 files changed, 79 insertions(+), 4 deletions(-) diff --git a/app/inpututils.cpp b/app/inpututils.cpp index 855e5b2c2..ff416949a 100644 --- a/app/inpututils.cpp +++ b/app/inpututils.cpp @@ -2105,6 +2105,29 @@ void InputUtils::updateQgisFormats( const QByteArray &output ) #endif } +QString InputUtils::getUniqueString( const QString &newString, const QStringList &existingStrings ) +{ + const QRegularExpression regex( QStringLiteral( R"(^(.*)\((\d+)\)$)" ) ); + QString uniqueString( newString.trimmed() ); + int i = 1; + while ( existingStrings.contains( uniqueString ) ) + { + // if the string ends with "(x)" where x is integer number replace it in place + auto regexMatch = regex.match( uniqueString ); + if ( regexMatch.hasMatch() ) + { + i = regexMatch.captured( 2 ).toInt() + 1; + uniqueString = QStringLiteral( "%1 (%2)" ).arg( regexMatch.captured( 1 ).trimmed(), QString::number( i ) ); + } + else + { + uniqueString = QStringLiteral( "%1 (%2)" ).arg( newString.trimmed(), QString::number( i ) ); + } + i++; + } + return uniqueString; +} + bool InputUtils::rescaleImage( const QString &path, QgsProject *activeProject ) { int quality = activeProject->readNumEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PhotoQuality" ), 0 ); diff --git a/app/inpututils.h b/app/inpututils.h index d94e1803e..adda3cb60 100644 --- a/app/inpututils.h +++ b/app/inpututils.h @@ -662,6 +662,19 @@ class InputUtils: public QObject */ static void updateQgisFormats( const QByteArray &output ); + /** + * From received string checks occurrences in existingStrings and returns first next unique option. Appends " (i)" + * behind received string or replaces it if it exists, where i is the smallest possible number starting from 1 if + * appending and next smallest if replacing. + * "lutra" -> "lutra (1)" + * "lutra (256)" -> "lutra (257)" + * " lutra " -> "lutra (1)" + * \param newString wanted string + * \param existingStrings QStringList of possible similar values + * \return unique variant of wanted string + */ + static QString getUniqueString( const QString &newString, const QStringList &existingStrings ); + public slots: void onQgsLogMessageReceived( const QString &message, const QString &tag, Qgis::MessageLevel level ); diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 509144c80..49a3d7aff 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -141,7 +141,17 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c if ( providerType == QStringLiteral( "external_ip" ) ) { - AbstractPositionProvider *provider = new NetworkPositionProvider( id, name, *mPositionTransformer ); + QString providerName( name ); + if ( providerName.isEmpty() ) + { + QStringList providerNames; + for ( const QVariant &provider : mAppSettings->savedPositionProviders() ) + { + providerNames << provider.toStringList().at( 0 ); + } + providerName = InputUtils::getUniqueString( tr( "Network device" ), providerNames ); + } + AbstractPositionProvider *provider = new NetworkPositionProvider( id, providerName, *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; } diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml index 943a962d7..c9dba8d93 100644 --- a/app/qml/gps/MMNetworkProviderDrawer.qml +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -115,10 +115,10 @@ MMComponents.MMDrawer { const deviceAddress = ip + ":" + port root.confirmed( aliasInput.text.trim(), deviceAddress ) + root.close() ipAddressInput.textField.clear() portInput.textField.clear() aliasInput.textField.clear() - root.close() } } } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index c808939aa..79305c397 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -78,7 +78,6 @@ MMComponents.MMPage { text: { if ( model.ProviderName ) return model.ProviderName - if ( model.ProviderType === "external_ip" ) return qsTr( "Network device" ) return qsTr( "Unknown device" ) } secondaryText: { @@ -164,7 +163,7 @@ MMComponents.MMPage { onConfirmed: function( alias, deviceAddress ) { PositionKit.positionProvider = PositionKit.constructProvider( "external_ip", deviceAddress, alias ) - providersModel.addProvider( alias, deviceAddress, "external_ip" ) + providersModel.addProvider( PositionKit.positionProvider.name(), deviceAddress, "external_ip" ) connectingDialogLoaderNetwork.open() } } diff --git a/app/test/testutilsfunctions.cpp b/app/test/testutilsfunctions.cpp index 556f0d3ac..2a55d201b 100644 --- a/app/test/testutilsfunctions.cpp +++ b/app/test/testutilsfunctions.cpp @@ -1047,3 +1047,32 @@ void TestUtilsFunctions::testSanitizePath() InputUtils::sanitizePath( str ); QCOMPARE( str, QStringLiteral( "C:/project name/project.qgz" ) ); } + +void TestUtilsFunctions::testUniqueString() +{ + const QStringList similarStrings( {QStringLiteral( "lutra" ), QStringLiteral( "lutra (1)" ), QStringLiteral( "lutra (256)" ), QStringLiteral( "lutra (257)" )} ); + + const QString testString1( QStringLiteral( "lutra" ) ); + QString result = InputUtils::getUniqueString( testString1, similarStrings ); + QCOMPARE( result, QStringLiteral( "lutra (2)" ) ); + + const QString testString2( QStringLiteral( "lutra " ) ); + result = InputUtils::getUniqueString( testString2, similarStrings ); + QCOMPARE( result, QStringLiteral( "lutra (2)" ) ); + + const QString testString3( QStringLiteral( " lutra" ) ); + result = InputUtils::getUniqueString( testString3, similarStrings ); + QCOMPARE( result, QStringLiteral( "lutra (2)" ) ); + + const QString testString4( QStringLiteral( "lutra (1)" ) ); + result = InputUtils::getUniqueString( testString4, similarStrings ); + QCOMPARE( result, QStringLiteral( "lutra (2)" ) ); + + const QString testString5( QStringLiteral( "lutra (256)" ) ); + result = InputUtils::getUniqueString( testString5, similarStrings ); + QCOMPARE( result, QStringLiteral( "lutra (258)" ) ); + + const QString testString6( QStringLiteral( "mulutram" ) ); + result = InputUtils::getUniqueString( testString6, similarStrings ); + QCOMPARE( result, QStringLiteral( "mulutram" ) ); +} diff --git a/app/test/testutilsfunctions.h b/app/test/testutilsfunctions.h index affe9437d..09839fecb 100644 --- a/app/test/testutilsfunctions.h +++ b/app/test/testutilsfunctions.h @@ -52,6 +52,7 @@ class TestUtilsFunctions: public QObject void testRelevantGeometryCenterToScreenCoordinates(); void testIsValidEmail(); void testSanitizePath(); + void testUniqueString(); private: void testFormatDuration( const QDateTime &t0, qint64 diffSecs, const QString &expectedResult ); From eafd434f846c635f9cc77045121b148c7c1298ee Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 29 Apr 2026 10:45:46 +0200 Subject: [PATCH 24/30] Fix position for local geoids --- app/position/providers/networkpositionprovider.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 2ed7d9950..8499aa40f 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -146,7 +146,7 @@ void NetworkPositionProvider::positionUpdateReceived() const QgsGpsInformation gpsInfo = mNmeaParser.parseNmeaString( nmeaData ); GeoPosition transformedPosition = mPositionTransformer->processNetworkPosition( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); - emit positionChanged( GeoPosition::fromQgsGpsInformation( gpsInfo ) ); + emit positionChanged( transformedPosition ); } void NetworkPositionProvider::socketStateChanged( const QAbstractSocket::SocketState state ) From 8a0438cfdf7cae497307ad9ad2e61074767d739d Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 29 Apr 2026 11:11:40 +0200 Subject: [PATCH 25/30] Fix crash on app exit --- .../providers/networkpositionprovider.cpp | 15 ++++++++++----- app/position/providers/networkpositionprovider.h | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index 8499aa40f..c2095ebc7 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -69,21 +69,26 @@ void NetworkPositionProvider::stopUpdates() } } +NetworkPositionProvider::~NetworkPositionProvider() +{ + NetworkPositionProvider::closeProvider(); +} + void NetworkPositionProvider::closeProvider() { + mUdpReconnectTimer.stop(); + mReconnectTimer.stop(); + if ( mTcpSocket ) { - mTcpSocket->close(); mTcpSocket->disconnect(); + mTcpSocket->abort(); } if ( mUdpSocket ) { - mUdpSocket->close(); mUdpSocket->disconnect(); + mUdpSocket->abort(); } - - mUdpReconnectTimer.stop(); - mReconnectTimer.stop(); } void NetworkPositionProvider::positionUpdateReceived() diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index e87c1f36a..300823e65 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -33,6 +33,7 @@ class NetworkPositionProvider : public AbstractPositionProvider public: NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); + ~NetworkPositionProvider() override; void startUpdates() override; void stopUpdates() override; From 1fb9741e5d06b0c39780c1002a213db8e9fd9873 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 29 Apr 2026 15:49:54 +0200 Subject: [PATCH 26/30] Fix bug where we changed BT providers to IP on restart --- app/position/providers/positionprovidersmodel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index c9a91b6ea..598b4b939 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -197,7 +197,8 @@ void PositionProvidersModel::setAppSettings( AppSettings *as ) } PositionProvider provider; - const QString providerType = providerData[2].isNull() ? QStringLiteral( "external_bt" ) : QStringLiteral( "external_ip" ); + // when migrating from older version where type wasn't saved we know it's bluetooth device + const QString providerType = providerData[2].isNull() ? QStringLiteral( "external_bt" ) : providerData[2].toString(); const QString deviceDesc = providerType == QStringLiteral( "external_bt" ) ? tr( " Bluetooth device" ) : tr( " Network device" ); provider.name = providerData[0].toString(); provider.providerId = providerData[1].toString(); From 2051a18013f42f7a28ca49435cea3e25793984b8 Mon Sep 17 00:00:00 2001 From: Matej Bagar Date: Wed, 29 Apr 2026 21:11:16 +0200 Subject: [PATCH 27/30] Improve code --- app/position/positionkit.cpp | 107 ++++++++---------- app/position/positionkit.h | 2 +- .../providers/networkpositionprovider.cpp | 6 +- app/position/providers/nmeaparser.h | 2 +- .../providers/positionprovidersmodel.cpp | 20 ++-- .../providers/positionprovidersmodel.h | 17 ++- 6 files changed, 72 insertions(+), 82 deletions(-) diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 49a3d7aff..8484cdbc0 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -7,28 +7,26 @@ * * ***************************************************************************/ -#include "position/positionkit.h" +#include "positionkit.h" + +#include + #include "coreutils.h" #include "mmconfig.h" -#include "qgis.h" +#include "appsettings.h" +#include "inpututils.h" #ifdef HAVE_BLUETOOTH -#include "position/providers/bluetoothpositionprovider.h" +#include "providers/bluetoothpositionprovider.h" #endif - -#include "position/providers/internalpositionprovider.h" -#include "position/providers/simulatedpositionprovider.h" +#include "providers/internalpositionprovider.h" +#include "providers/simulatedpositionprovider.h" #include "providers/networkpositionprovider.h" #ifdef ANDROID -#include "position/providers/androidpositionprovider.h" +#include "providers/androidpositionprovider.h" #include #endif -#include "appsettings.h" -#include "inpututils.h" - -#include - PositionKit::PositionKit( QObject *parent ) : QObject( parent ) { @@ -127,19 +125,17 @@ QString PositionKit::positionProviderName() const AbstractPositionProvider *PositionKit::constructProvider( const QString &type, const QString &id, const QString &name ) { - QString providerType( type ); - if ( providerType == QStringLiteral( "external_bt" ) ) - { + #ifdef HAVE_BLUETOOTH + if ( type == QStringLiteral( "external_bt" ) ) + { AbstractPositionProvider *provider = new BluetoothPositionProvider( id, name, *mPositionTransformer ); QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); return provider; -#else - providerType = QStringLiteral( "internal" ); -#endif } +#endif - if ( providerType == QStringLiteral( "external_ip" ) ) + if ( type == QStringLiteral( "external_ip" ) ) { QString providerName( name ); if ( providerName.isEmpty() ) @@ -156,7 +152,8 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c return provider; } - // type == internal + // from this point type == internal + if ( id == QStringLiteral( "simulated" ) ) { AbstractPositionProvider *provider = new SimulatedPositionProvider( *mPositionTransformer ); @@ -192,12 +189,12 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c } } -AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *appsettings ) +AbstractPositionProvider *PositionKit::constructActiveProvider( const AppSettings *appsettings ) { if ( !appsettings ) return nullptr; - QString providerId = appsettings->activePositionProviderId(); + const QString &providerId = appsettings->activePositionProviderId(); if ( providerId.isEmpty() ) // nothing has been written to qsettings { @@ -209,45 +206,41 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( AppSettings *app return constructProvider( QStringLiteral( "internal" ), QStringLiteral( "devicegps" ) ); #endif } - else // desktop - { - return constructProvider( QStringLiteral( "internal" ), QStringLiteral( "simulated" ) ); - } + // desktop + return constructProvider( QStringLiteral( "internal" ), QStringLiteral( "simulated" ) ); } - else if ( providerId == QStringLiteral( "devicegps" ) || providerId == QStringLiteral( "simulated" ) || - providerId == QStringLiteral( "android_fused" ) || providerId == QStringLiteral( "android_gps" ) ) + if ( providerId == QStringLiteral( "devicegps" ) || providerId == QStringLiteral( "simulated" ) || + providerId == QStringLiteral( "android_fused" ) || providerId == QStringLiteral( "android_gps" ) ) { return constructProvider( QStringLiteral( "internal" ), providerId ); } - else - { - // find name & type of the active provider - QString providerName; - // Migration from single external provider to multiple currently, missing type == bluetooth provider - QString providerType = QStringLiteral( "external_bt" ); - QVariantList providers = appsettings->savedPositionProviders(); - for ( const auto &provider : providers ) + // find name & type of the active provider + QString providerName; + // Migration from single external provider to multiple currently, missing type == bluetooth provider + QString providerType = QStringLiteral( "external_bt" ); + QVariantList providers = appsettings->savedPositionProviders(); + + for ( const QVariant &provider : providers ) + { + QVariantList providerData = provider.toList(); + if ( providerData.length() < 2 ) { - QVariantList providerData = provider.toList(); - if ( providerData.length() < 2 ) - { - CoreUtils::log( QStringLiteral( "PositionKit" ), QStringLiteral( "Found provider with insufficient data" ) ); - continue; - } + CoreUtils::log( QStringLiteral( "PositionKit" ), QStringLiteral( "Found provider with insufficient data" ) ); + continue; + } - if ( providerData[1] == providerId ) + if ( providerData[1] == providerId ) + { + providerName = providerData[0].toString(); + if ( !providerData.at( 2 ).isNull() ) { - providerName = providerData[0].toString(); - if ( !providerData.at( 2 ).isNull() ) - { - providerType = providerData[2].toString(); - } + providerType = providerData[2].toString(); } } - - return constructProvider( providerType, providerId, providerName ); } + + return constructProvider( providerType, providerId, providerName ); } void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) @@ -427,7 +420,7 @@ void PositionKit::setSkipElevationTransformation( const bool skipElevationTransf mSkipElevationTransformation = skipElevationTransformation; } -void PositionKit::appStateChanged( Qt::ApplicationState state ) +void PositionKit::appStateChanged( const Qt::ApplicationState state ) { if ( state == Qt::ApplicationActive ) { @@ -469,9 +462,9 @@ double PositionKit::geoidSeparation() const QgsPoint PositionKit::positionCoordinate() const { if ( mPosition.hasValidPosition() ) - return QgsPoint( mPosition.longitude, mPosition.latitude, mPosition.elevation ); + return { mPosition.longitude, mPosition.latitude, mPosition.elevation }; - return QgsPoint(); + return {}; } bool PositionKit::hasPosition() const @@ -576,7 +569,7 @@ void PositionKit::setAppSettings( AppSettings *appSettings ) mAppSettings = appSettings; if ( mAppSettings ) { - QObject::connect( mAppSettings, &AppSettings::gpsAntennaHeightChanged, this, &PositionKit::antennaHeightChanged ); + connect( mAppSettings, &AppSettings::gpsAntennaHeightChanged, this, &PositionKit::antennaHeightChanged ); } emit appSettingsChanged(); emit antennaHeightChanged(); @@ -589,8 +582,6 @@ double PositionKit::antennaHeight() const { return mAppSettings->gpsAntennaHeight(); } - else - { - return 0; - } + + return 0; } diff --git a/app/position/positionkit.h b/app/position/positionkit.h index b0ab11de9..6c9bc006f 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -137,7 +137,7 @@ class PositionKit : public QObject Q_INVOKABLE QString positionCrs3DGeoidModelName(); Q_INVOKABLE AbstractPositionProvider *constructProvider( const QString &type, const QString &id, const QString &name = QString() ); - Q_INVOKABLE AbstractPositionProvider *constructActiveProvider( AppSettings *appsettings ); + Q_INVOKABLE AbstractPositionProvider *constructActiveProvider( const AppSettings *appsettings ); AppSettings *appSettings() const; void setAppSettings( AppSettings *appSettings ); diff --git a/app/position/providers/networkpositionprovider.cpp b/app/position/providers/networkpositionprovider.cpp index c2095ebc7..13708470f 100644 --- a/app/position/providers/networkpositionprovider.cpp +++ b/app/position/providers/networkpositionprovider.cpp @@ -12,13 +12,13 @@ #include #include -static int ONE_SECOND_MS = 1000; +constexpr int ONE_SECOND_MS = 1000; NetworkPositionProvider::NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) : AbstractPositionProvider( addr, QStringLiteral( "external_ip" ), name, positionTransformer, parent ), mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) { - const QStringList targetAddress = addr.split( ":" ); + const QStringList targetAddress = addr.split( QStringLiteral( ":" ) ); mTargetAddress = targetAddress.at( 0 ); mTargetPort = targetAddress.at( 1 ).toInt(); @@ -203,7 +203,7 @@ void NetworkPositionProvider::reconnectTimeout() QString NetworkPositionProvider::getIpAddress() const { - return mTargetAddress + ":" + QString::number( mTargetPort ); + return QStringLiteral( "%1:%2" ).arg( mTargetAddress, QString::number( mTargetPort ) ); } void NetworkPositionProvider::reconnect() diff --git a/app/position/providers/nmeaparser.h b/app/position/providers/nmeaparser.h index fc47d7e82..b50f9f3b3 100644 --- a/app/position/providers/nmeaparser.h +++ b/app/position/providers/nmeaparser.h @@ -13,7 +13,7 @@ #include /** - * NmeaParser is a big hack how to reuse QGIS NmeaConnection function in order to (a) keep ownership of bluetooth + * NmeaParser is a big hack how to reuse QGIS NmeaConnection function in order to (a) keep ownership of bluetooth / TCP/UDP * socket, (b) do not have multiple unique_ptrs holding the same pointer and to avoid some possible crashes. * * Note: This way of reusing makes the parser highly dependent on QgsNmeaConnection class and any change inside the class diff --git a/app/position/providers/positionprovidersmodel.cpp b/app/position/providers/positionprovidersmodel.cpp index 598b4b939..0c6a02f95 100644 --- a/app/position/providers/positionprovidersmodel.cpp +++ b/app/position/providers/positionprovidersmodel.cpp @@ -19,7 +19,7 @@ PositionProvidersModel::PositionProvidersModel( QObject *parent ) : QAbstractLis { if ( !InputUtils::isMobilePlatform() ) { - PositionProvider simulated( "Simulated provider", "Simulated position around point", "internal", "simulated" ); + const PositionProvider simulated( "Simulated provider", "Simulated position around point", "internal", "simulated" ); mProviders.push_front( simulated ); } @@ -74,20 +74,20 @@ QHash PositionProvidersModel::roleNames() const int PositionProvidersModel::rowCount( const QModelIndex & ) const { - return mProviders.count(); + return static_cast( mProviders.count() ); } -QVariant PositionProvidersModel::data( const QModelIndex &index, int role ) const +QVariant PositionProvidersModel::data( const QModelIndex &index, const int role ) const { if ( !index.isValid() ) - return QVariant(); + return {}; - int row = index.row(); + const int row = index.row(); PositionProvider provider = mProviders.at( row ); if ( row < 0 || row > mProviders.count() ) - return QVariant(); + return {}; switch ( role ) { @@ -104,7 +104,7 @@ QVariant PositionProvidersModel::data( const QModelIndex &index, int role ) cons return provider.providerType; default: - return QVariant(); + return {}; } } @@ -119,7 +119,7 @@ void PositionProvidersModel::removeProvider( const QString &providerId ) if ( !mProviders.contains( toRemove ) ) return; - int removeIndex = mProviders.indexOf( toRemove ); + const int removeIndex = static_cast( mProviders.indexOf( toRemove ) ); beginRemoveRows( QModelIndex(), removeIndex, removeIndex ); @@ -149,7 +149,7 @@ void PositionProvidersModel::addProvider( const QString &name, const QString &pr if ( mProviders.contains( toAdd ) ) return; - int addIndex = mProviders.count(); + const int addIndex = static_cast( mProviders.count() ); beginInsertRows( QModelIndex(), addIndex, addIndex ); @@ -186,7 +186,7 @@ void PositionProvidersModel::setAppSettings( AppSettings *as ) for ( int i = 0; i < providers.count(); ++i ) { - if ( providers[i].type() == QVariant::List || providers[i].type() == QVariant::StringList ) + if ( providers[i].typeId() == QMetaType::QVariantList || providers[i].typeId() == QMetaType::QStringList ) { QVariantList providerData = providers[i].toList(); diff --git a/app/position/providers/positionprovidersmodel.h b/app/position/providers/positionprovidersmodel.h index a05589504..6499038ed 100644 --- a/app/position/providers/positionprovidersmodel.h +++ b/app/position/providers/positionprovidersmodel.h @@ -10,7 +10,6 @@ #ifndef POSITIONPROVIDERSMODEL_H #define POSITIONPROVIDERSMODEL_H -#include #include #include @@ -21,8 +20,8 @@ struct PositionProvider { QString name; QString description; - QString providerType; // either internal or external - QString providerId; // holds BT address for external provider and simulated/internal for internal provider + QString providerType; // can be internal, external_bt, external_ip + QString providerId; // holds BT address or IP address for external providers and simulated/internal for internal providers bool operator==( const PositionProvider &other ) const { @@ -38,7 +37,7 @@ struct PositionProvider : name( name ), description( desc ), providerType( type ), providerId( id ) {} - PositionProvider() {} + PositionProvider() = default; }; class PositionProvidersModel : public QAbstractListModel @@ -50,14 +49,14 @@ class PositionProvidersModel : public QAbstractListModel public: explicit PositionProvidersModel( QObject *parent = nullptr ); - virtual ~PositionProvidersModel(); + ~PositionProvidersModel() override; enum DataRoles { - ProviderName = Qt::UserRole + 1, //! physical address of BT device - ProviderDescription, - ProviderId, - ProviderType // external (connected) / internal (device) + ProviderName = Qt::UserRole + 1, // name of bluetooth device or custom name for network device + ProviderDescription, // device address (IP/BT) + device type + ProviderId, // device address (IP/BT) + ProviderType // external_ip (connected) / external_bt (connected) / internal (device) / simulated (device) }; Q_ENUM( DataRoles ) From a466c7b674177b0b8ee5e576d5f9235ff5327603 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Tue, 5 May 2026 12:32:17 +0300 Subject: [PATCH 28/30] Fixed first round of review findings --- app/qml/CMakeLists.txt | 4 +- app/qml/account/MMHowYouFoundUsPage.qml | 2 +- .../components/MMIconCheckBoxHorizontal.qml | 60 +++----- ...awer.qml => MMBluetoothProviderDrawer.qml} | 11 +- .../MMExternalProviderConnectionDrawer.qml | 142 ++++++------------ app/qml/gps/MMGpsDataDrawer.qml | 5 +- app/qml/gps/MMNetworkProviderDrawer.qml | 8 +- app/qml/gps/MMPositionProviderPage.qml | 6 +- app/qml/gps/MMProviderTypeDrawer.qml | 5 +- gallery/qml.qrc | 2 +- gallery/qml/pages/ChecksPage.qml | 8 +- 11 files changed, 93 insertions(+), 160 deletions(-) rename app/qml/{account => }/components/MMIconCheckBoxHorizontal.qml (52%) rename app/qml/gps/{MMAddPositionProviderDrawer.qml => MMBluetoothProviderDrawer.qml} (94%) diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt index 280d40a62..12b847168 100644 --- a/app/qml/CMakeLists.txt +++ b/app/qml/CMakeLists.txt @@ -1,5 +1,4 @@ set(MM_QML - account/components/MMIconCheckBoxHorizontal.qml account/components/MMIconCheckBoxVertical.qml account/components/MMAccountPageItem.qml account/components/MMWorkspaceDelegate.qml @@ -26,6 +25,7 @@ set(MM_QML components/MMDrawerHeader.qml components/MMHlineText.qml components/MMIcon.qml + components/MMIconCheckBoxHorizontal.qml components/MMInfoBox.qml components/MMLine.qml components/MMListDelegate.qml @@ -132,7 +132,7 @@ set(MM_QML form/editors/MMFormTextMultilineEditor.qml form/editors/MMFormValueMapEditor.qml form/editors/MMFormValueRelationEditor.qml - gps/MMAddPositionProviderDrawer.qml + gps/MMBluetoothProviderDrawer.qml gps/MMExternalProviderConnectionDrawer.qml gps/MMGpsDataDrawer.qml gps/MMNetworkProviderDrawer.qml diff --git a/app/qml/account/MMHowYouFoundUsPage.qml b/app/qml/account/MMHowYouFoundUsPage.qml index 66a84031e..dbc6cfe86 100644 --- a/app/qml/account/MMHowYouFoundUsPage.qml +++ b/app/qml/account/MMHowYouFoundUsPage.qml @@ -112,7 +112,7 @@ MMPage { NumberAnimation { properties: "x,y"; duration: 100 } } - delegate: MMAccountComponents.MMIconCheckBoxHorizontal { + delegate: MMIconCheckBoxHorizontal{ x: model.submenu ? __style.margin20 : 0 width: model.submenu ? ListView.view.width - __style.margin20 : ListView.view.width diff --git a/app/qml/account/components/MMIconCheckBoxHorizontal.qml b/app/qml/components/MMIconCheckBoxHorizontal.qml similarity index 52% rename from app/qml/account/components/MMIconCheckBoxHorizontal.qml rename to app/qml/components/MMIconCheckBoxHorizontal.qml index f9327bb18..45dff5de6 100644 --- a/app/qml/account/components/MMIconCheckBoxHorizontal.qml +++ b/app/qml/components/MMIconCheckBoxHorizontal.qml @@ -11,81 +11,69 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic -import "../../components" - CheckBox { id: control - property string sourceIcon: "" - property string description: "" + property alias sourceIcon: icon.source + property alias description: descriptionText.text property bool showBorder: false property bool small: false - height: (description !== "" ? 96 : (control.small ? 50 : 80)) * __dp + readonly property int baseHeight: small ? 50 : 80 + + height: control.baseHeight * __dp + implicitWidth: leftPadding + titleText.implicitWidth + rightPadding - leftPadding: (description !== "" ? iconBgRectangle.x : 0) + iconBgRectangle.width + 30 * __dp + topPadding: Math.max(0, (height - contentColumn.implicitHeight) / 2) + bottomPadding: topPadding + leftPadding: iconBgRectangle.x + iconBgRectangle.width + 20 * __dp rightPadding: 20 * __dp indicator: Rectangle { id: iconBgRectangle - width: (control.small ? 24 : (control.description !== "" ? 50 : 40)) * __dp - height: (control.small ? 24 : (control.description !== "" ? 50 : 40)) * __dp + implicitWidth: (small ? 24 : 40) * __dp + implicitHeight: (small ? 24 : 40) * __dp x: 20 * __dp y: control.height / 2 - height / 2 radius: width / 2 color: control.checked ? __style.polarColor : __style.lightGreenColor MMIcon { - size: control.small ? __style.icon16 : __style.icon24 + id: icon + size: small ? __style.icon16 : __style.icon24 anchors.centerIn: parent source: control.sourceIcon color: __style.forestColor } } - contentItem: Item { - implicitWidth: titleText.implicitWidth + contentItem: Column { + id: contentColumn + spacing: 4 * __dp Text { id: titleText - visible: control.description === "" width: parent.width - height: parent.height text: control.text font: __style.t3 color: control.checked ? __style.polarColor : __style.nightColor elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter } - Column { - id: textColumn - visible: control.description !== "" - anchors.verticalCenter: parent.verticalCenter + Text { + id: descriptionText + visible: text !== "" width: parent.width - spacing: 10 * __dp - - Text { - width: parent.width - text: control.text - font: __style.t3 - color: control.checked ? __style.polarColor : __style.nightColor - elide: Text.ElideRight - } - - Text { - width: parent.width - text: control.description - font: __style.p6 - color: control.checked ? __style.polarColor : __style.nightColor - elide: Text.ElideRight - } + font: __style.p6 + color: control.checked ? __style.polarColor : __style.nightColor + wrapMode: Text.Wrap + maximumLineCount: 2 } } background: Rectangle { radius: __style.radius12 color: control.checked ? __style.forestColor : __style.polarColor - border.color: showBorder ? ( control.checked ? __style.transparentColor : __style.mediumGreenColor ) : __style.transparentColor + border.color: showBorder && !control.checked ? __style.mediumGreenColor : __style.transparentColor } } diff --git a/app/qml/gps/MMAddPositionProviderDrawer.qml b/app/qml/gps/MMBluetoothProviderDrawer.qml similarity index 94% rename from app/qml/gps/MMAddPositionProviderDrawer.qml rename to app/qml/gps/MMBluetoothProviderDrawer.qml index 65ccf97f1..1263b203b 100644 --- a/app/qml/gps/MMAddPositionProviderDrawer.qml +++ b/app/qml/gps/MMBluetoothProviderDrawer.qml @@ -10,7 +10,6 @@ import QtCore import QtQuick import QtQuick.Controls - import mm 1.0 as MM import "../components" as MMComponents @@ -115,20 +114,19 @@ MMComponents.MMListDrawer { anchors.horizontalCenter: parent.horizontalCenter width: parent.width - spacing: __style.spacing16 Image { anchors.horizontalCenter: parent.horizontalCenter source: __style.mmSymbolImage - width: 32 * __dp - height: 32 * __dp + width: __style.icon32 * __dp + height: __style.icon32 * __dp fillMode: Image.PreserveAspectFit } - MMComponents.MMListSpacer { height: __style.margin20 } + MMComponents.MMListSpacer { height: __style.margin16 } MMComponents.MMText { width: parent.width @@ -140,8 +138,7 @@ MMComponents.MMListDrawer { horizontalAlignment: Text.AlignHCenter } - - MMComponents.MMListSpacer { height: __style.margin20 } + MMComponents.MMListSpacer { height: __style.margin16 } } } } diff --git a/app/qml/gps/MMExternalProviderConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml index eec55a508..928aba748 100644 --- a/app/qml/gps/MMExternalProviderConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -10,7 +10,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts - import mm 1.0 as MM import MMInput @@ -21,93 +20,9 @@ MMComponents.MMDrawer { property string providerType: "" property var positionProvider: PositionKit.positionProvider - property string howToConnectGPSLink: __inputHelp.howToConnectGPSLink - - property string titleText: { - if ( root.providerType === "network" ) - { - if ( rootstate.state === "working" ) - { - if ( !root.positionProvider ) return "" - return qsTr( "Connecting to external receiver" ) - } - else if ( rootstate.state === "success" ) - { - return qsTr( "Connected" ) - } - else if ( rootstate.state === "fail" ) - { - return qsTr( "Failed to connect to" ) + " " + ( root.positionProvider ? root.positionProvider.getIpAddress() : "" ) - } - else return qsTr( "We were not able to connect to the specified IP address. Please try again later." ) - } - else if ( root.providerType === "bluetooth" ) - { - if ( rootstate.state === "working" ) - { - if ( !root.positionProvider ) return "" - if ( root.positionProvider.name() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.name() - return qsTr( "Connecting" ) + connectingSuffixAnimation - } - else if ( rootstate.state === "success" ) - { - return qsTr( "Connected" ) - } - else if ( rootstate.state === "fail" ) - { - return qsTr( "Failed to connect to" ) + " " + ( root.positionProvider ? root.positionProvider.name() : "" ) - } - else - { - if ( root.providerType === "bluetooth" ) return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) - } - } - } property string connectingSuffixAnimation: "" - property string descriptionText: { - if ( rootstate.state === "working" ) - { - if ( root.providerType === "bluetooth" ) - return qsTr( "You might be asked to pair your device during this process." ) - else if ( root.providerType === "network" ) - if ( root.positionProvider.getIpAddress() ) return qsTr( "Connecting to" ) + " " + root.positionProvider.getIpAddress() + ". You can close this panel, the app will continue in the background." - return qsTr( "Connecting" ) + connectingSuffixAnimation - } - else if ( rootstate.state === "success" ) - { - return "" - } - else if ( rootstate.state === "waitingToReconnect" ) - { - return PositionKit.positionProvider.stateMessage + "

" + - qsTr( "You can close this message, we will try to repeatedly connect to your device." ) - } - - else - { - if ( root.providerType === "bluetooth" ) - return qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) - else if ( root.providerType === "network" ) - return qsTr( "We were not able to connect to the specified IP address. Please try again later." ) - } - } - - property var imageSource: { - if ( rootstate.state === "fail" ) - { - return __style.externalGpsRedImage - } - else - { - if ( root.providerType === "bluetooth" ) - return __style.externalBluetoothGreenImage - else if ( root.providerType === "network" ) - return __style.externalNetworkGreenImage - } - } - signal success() signal failure() @@ -118,18 +33,60 @@ MMComponents.MMDrawer { State { name: "working" when: root.positionProvider && root.positionProvider.state === MM.PositionProvider.Connecting + PropertyChanges { + target: message + image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage + title: root.providerType === "network" + ? qsTr( "Connecting to external receiver" ) + : ( root.positionProvider.name() + ? qsTr( "Connecting to" ) + " " + root.positionProvider.name() + : qsTr( "Connecting" ) + root.connectingSuffixAnimation ) + description: root.providerType === "bluetooth" + ? qsTr( "You might be asked to pair your device during this process." ) + : ( root.positionProvider.getIpAddress() + ? qsTr( "Connecting to" ) + " " + root.positionProvider.getIpAddress() + qsTr( ". You can close this panel, the app will continue in the background." ) + : qsTr( "Connecting" ) + root.connectingSuffixAnimation ) + linkText: "" + } }, State { name: "success" when: root.positionProvider && root.positionProvider.state === MM.PositionProvider.Connected + PropertyChanges { + target: message + image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage + title: qsTr( "Connected" ) + description: "" + linkText: "" + } }, State { name: "fail" when: !root.positionProvider || root.positionProvider.state === MM.PositionProvider.NoConnection + PropertyChanges { + target: message + image: __style.externalGpsRedImage + title: qsTr( "Failed to connect to" ) + " " + ( root.positionProvider + ? ( root.providerType === "network" ? root.positionProvider.getIpAddress() : root.positionProvider.name() ) + : "" ) + description: root.providerType === "bluetooth" + ? qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) + : qsTr( "We were not able to connect to the specified IP address. Please try again later." ) + linkText: qsTr( "Learn more" ) + } }, State { name: "waitingToReconnect" - when: !root.positionProvider || root.positionProvider.state === MM.PositionProvider.WaitingToReconnect + when: root.positionProvider && root.positionProvider.state === MM.PositionProvider.WaitingToReconnect + PropertyChanges { + target: message + image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage + title: root.providerType === "bluetooth" + ? qsTr( "We were not able to connect to the specified device. Please make sure your device is powered on and can be connected to." ) + : qsTr( "We were not able to connect to the specified IP address. Please try again later." ) + description: root.positionProvider.stateMessage + "

" + qsTr( "You can close this message, we will try to repeatedly connect to your device." ) + linkText: qsTr( "Learn more" ) + } } ] @@ -143,13 +100,10 @@ MMComponents.MMDrawer { height: root.maxHeightHit ? root.drawerContentAvailableHeight : contentHeight MMComponents.MMMessage { - width: parent.width + id: message - image: root.imageSource - title: root.titleText - description: root.descriptionText - link: root.howToConnectGPSLink - linkText: ( rootstate.state === "working" || rootstate.state === "success" ) ? "" : qsTr( "Learn more" ) + width: parent.width + link: __inputHelp.howToConnectGPSLink } } @@ -159,9 +113,7 @@ MMComponents.MMDrawer { interval: 1500 repeat: false running: rootstate.state === "success" - onTriggered: { - root.close() - } + onTriggered: root.close() } Timer { diff --git a/app/qml/gps/MMGpsDataDrawer.qml b/app/qml/gps/MMGpsDataDrawer.qml index 3265edc86..e14568956 100644 --- a/app/qml/gps/MMGpsDataDrawer.qml +++ b/app/qml/gps/MMGpsDataDrawer.qml @@ -9,7 +9,6 @@ import QtQuick import QtQuick.Layouts - import mm 1.0 as MM import MMInput @@ -34,13 +33,13 @@ MMComponents.MMDrawer { width: parent.width height: root.maxHeightHit ? root.drawerContentAvailableHeight - : ( scrollView.contentHeight + reservedButtonSpace ) + : ( scrollView.contentHeight + root.reservedButtonSpace ) MMComponents.MMScrollView { id: scrollView width: parent.width - height: root.maxHeightHit ? ( parent.height - reservedButtonSpace) + height: root.maxHeightHit ? ( parent.height - root.reservedButtonSpace) : contentHeight Column { diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml index c9dba8d93..aa87a3d86 100644 --- a/app/qml/gps/MMNetworkProviderDrawer.qml +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -57,8 +57,8 @@ MMComponents.MMDrawer { width: parent.width textFieldBackground.color: __style.lightGreenColor - title: qsTr( "Port" ) - placeholderText: qsTr( "1234" ) + title: "Port" + placeholderText: "1234" textField.inputMethodHints: Qt.ImhDigitsOnly @@ -98,7 +98,7 @@ MMComponents.MMDrawer { } const portNum = parseInt( port ) - if ( port === "" ) { + if ( !port ) { portInput.errorMsg = qsTr( "Port is required" ) } else if ( !/^\d+$/.test( port ) || portNum < 1 || portNum > 65535 ) { @@ -108,7 +108,7 @@ MMComponents.MMDrawer { portInput.errorMsg = "" } - if ( ipAddressInput.errorMsg !== "" || portInput.errorMsg !== "" ) { + if ( ipAddressInput.errorMsg || portInput.errorMsg ) { return } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 79305c397..c38829b91 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -169,8 +169,6 @@ MMComponents.MMPage { } MMComponents.MMMessage { - id: infoMessage - visible: !listview.visible width: parent.width anchors.centerIn: parent @@ -194,7 +192,7 @@ MMComponents.MMPage { } onRemoveProvider: { - if ( removeDialog.providerId === "" ) { + if ( !removeDialog.providerId ) { close() return } @@ -215,7 +213,7 @@ MMComponents.MMPage { id: bluetoothDiscoveryLoader active: false - source: Qt.resolvedUrl( "MMAddPositionProviderDrawer.qml" ) + source: Qt.resolvedUrl( "MMBluetoothProviderDrawer.qml" ) onLoaded: item.open() } diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index 60137d62f..284b59c04 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -8,9 +8,8 @@ ***************************************************************************/ import QtQuick - +import QtQml.Models import "../components" as MMComponents -import "../account/components" as MMAccountComponents MMComponents.MMListDrawer { id: root @@ -64,7 +63,7 @@ MMComponents.MMListDrawer { width: ListView.view.width height: checkbox.height + __style.margin12 - MMAccountComponents.MMIconCheckBoxHorizontal { + MMComponents.MMIconCheckBoxHorizontal{ id: checkbox width: parent.width diff --git a/gallery/qml.qrc b/gallery/qml.qrc index 86382e3f3..ece26deed 100644 --- a/gallery/qml.qrc +++ b/gallery/qml.qrc @@ -40,6 +40,7 @@ ../app/qml/components/MMPageHeader.qml ../app/qml/components/MMDrawerHeader.qml ../app/qml/components/MMLine.qml + ../app/qml/components/MMIconCheckBoxHorizontal.qml ../app/qml/components/MMIcon.qml ../app/qml/components/MMLine.qml ../app/qml/components/MMText.qml @@ -79,7 +80,6 @@ ../app/qml/components/MMSingleClickMouseArea.qml ../app/qml/components/MMNotificationBox.qml ../app/qml/account/components/MMAccountPageItem.qml - ../app/qml/account/components/MMIconCheckBoxHorizontal.qml ../app/qml/account/components/MMIconCheckBoxVertical.qml ../app/qml/account/components/MMWorkspaceDelegate.qml ../app/qml/account/components/MMWorkspaceInvitationDelegate.qml diff --git a/gallery/qml/pages/ChecksPage.qml b/gallery/qml/pages/ChecksPage.qml index 301fec071..8f33550ad 100644 --- a/gallery/qml/pages/ChecksPage.qml +++ b/gallery/qml/pages/ChecksPage.qml @@ -73,26 +73,26 @@ ScrollView { spacing: 10 anchors.fill: parent - MMAccountComponents.MMIconCheckBoxHorizontal { + MMComponents.MMIconCheckBoxHorizontal{ checked: false sourceIcon: __style.qgisIcon text: "QGIS website" } - MMAccountComponents.MMIconCheckBoxHorizontal { + MMComponents.MMIconCheckBoxHorizontal{ checked: true sourceIcon: __style.qgisIcon text: "QGIS website" } - MMAccountComponents.MMIconCheckBoxHorizontal { + MMComponents.MMIconCheckBoxHorizontal{ checked: false sourceIcon: __style.redditIcon text: "Reddit" small: true } - MMAccountComponents.MMIconCheckBoxHorizontal { + MMComponents.MMIconCheckBoxHorizontal{ checked: true width: 300 * __dp sourceIcon: __style.redditIcon From d9433cfe931fa8b1b4ec146a7320e5d6188ec061 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Wed, 6 May 2026 09:12:58 +0300 Subject: [PATCH 29/30] Refactored Position provider page Implemented code review findings --- app/qml/components/MMListDelegate.qml | 2 +- app/qml/gps/MMGpsDataDrawer.qml | 8 +- app/qml/gps/MMNetworkProviderDrawer.qml | 7 +- app/qml/gps/MMPositionProviderPage.qml | 104 ++++++++---------------- app/qml/gps/MMProviderTypeDrawer.qml | 83 ++++++++++--------- gallery/qml/pages/ChecksPage.qml | 11 +++ 6 files changed, 92 insertions(+), 123 deletions(-) diff --git a/app/qml/components/MMListDelegate.qml b/app/qml/components/MMListDelegate.qml index b7869968c..93c94cca0 100644 --- a/app/qml/components/MMListDelegate.qml +++ b/app/qml/components/MMListDelegate.qml @@ -42,7 +42,7 @@ Item { implicitWidth: ListView?.view?.width ?? 0 // in case ListView is injected as attached property (usually it is) implicitHeight: contentLayout.implicitHeight - height: visible ? implicitHeight : 0.1 // hide invisible items, for some reason setting 0 does not work ¯\_(ツ)_/¯ + height: visible ? contentLayout.implicitHeight : 0.1 // hide invisible items, for some reason setting 0 does not work ¯\_(ツ)_/¯ MouseArea { anchors.fill: contentLayout diff --git a/app/qml/gps/MMGpsDataDrawer.qml b/app/qml/gps/MMGpsDataDrawer.qml index e14568956..0d32e3930 100644 --- a/app/qml/gps/MMGpsDataDrawer.qml +++ b/app/qml/gps/MMGpsDataDrawer.qml @@ -21,25 +21,22 @@ MMComponents.MMDrawer { property var mapSettings property bool showReceiversButton: true - readonly property real reservedButtonSpace: __style.spacing20 + manageButton.implicitHeight + __style.safeAreaBottom + __style.margin8 signal manageReceiversClicked() drawerHeader.title: qsTr( "GPS info" ) - drawerBottomMargin: 0 - drawerContent: Item { width: parent.width height: root.maxHeightHit ? root.drawerContentAvailableHeight - : ( scrollView.contentHeight + root.reservedButtonSpace ) + : ( scrollView.contentHeight + internal.reservedButtonSpace ) MMComponents.MMScrollView { id: scrollView width: parent.width - height: root.maxHeightHit ? ( parent.height - root.reservedButtonSpace) + height: root.maxHeightHit ? ( parent.height - internal.reservedButtonSpace) : contentHeight Column { @@ -373,5 +370,6 @@ MMComponents.MMDrawer { id: internal property string coordinatesInDegrees: __inputUtils.degreesString( PositionKit.positionCoordinate ) + readonly property real reservedButtonSpace: __style.spacing20 + manageButton.implicitHeight + __style.safeAreaBottom + __style.margin8 } } diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml index aa87a3d86..0a7090d48 100644 --- a/app/qml/gps/MMNetworkProviderDrawer.qml +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -26,17 +26,12 @@ MMComponents.MMDrawer { MMComponents.MMText { width: parent.width - text: qsTr( "Enter the IP address and port for your receiver. You can find these in your manufacturer's app. TCP and UDP connections are supported. Optionally, add a nickname to tell your receivers apart." ) font: __style.p5 color: __style.nightColor wrapMode: Text.Wrap horizontalAlignment: Text.AlignJustify - - onLinkActivated: function( link ) { - Qt.openUrlExternally( link ) - } } MMInputs.MMTextInput { @@ -87,7 +82,7 @@ MMComponents.MMDrawer { const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/ const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/ - if ( ip === "" ) { + if ( !ip ) { ipAddressInput.errorMsg = qsTr( "IP address is required" ) } else if ( !ipv4Regex.test( ip ) && !hostnameRegex.test( ip ) ) { diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index c38829b91..bcc801e7f 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -18,7 +18,7 @@ import "../dialogs" as MMDialogs MMComponents.MMPage { id: root - pageHeader.title: listview.showTopTitle ? internal.pageTitle : "" + pageHeader.title: internal.pageTitle pageBottomMarginPolicy: MMComponents.MMPage.PaintBehindSystemBar @@ -29,8 +29,6 @@ MMComponents.MMPage { MMComponents.MMListView { id: listview - property bool showTopTitle: visibleArea.yPosition * height > ( headerItem.contentHeight / 2 ) - width: parent.width height: parent.height @@ -42,26 +40,13 @@ MMComponents.MMPage { appSettings: AppSettings } - header: MMComponents.MMText { - id: headerText - - width: ListView.view.width - - text: internal.pageTitle - - font: __style.h3 - color: __style.forestColor - - wrapMode: Text.Wrap - maximumLineCount: 2 - - bottomPadding: __style.margin40 - } - delegate: MMComponents.MMListDelegate { id: listdelegate - property bool isActive: AppSettings.activePositionProviderId === model.ProviderId + required property var model + required property int index + + property bool isActive: AppSettings.activePositionProviderId === listdelegate.model.ProviderId leftContent: MMComponents.MMRadioButton { checked: listdelegate.isActive @@ -71,34 +56,34 @@ MMComponents.MMPage { anchors.fill: parent onClicked: function( mouse ) { mouse.accepted = true - root.constructProvider( model.ProviderType, model.ProviderId, model.ProviderName ) + root.activateProvider( listdelegate.model.ProviderType, listdelegate.model.ProviderId, listdelegate.model.ProviderName ) } } } text: { - if ( model.ProviderName ) return model.ProviderName + if ( listdelegate.model.ProviderName ) return listdelegate.model.ProviderName return qsTr( "Unknown device" ) } secondaryText: { if ( listdelegate.isActive ) { - if ( model.ProviderType === "external_ip" ) + if ( listdelegate.model.ProviderType === "external_ip" && typeof PositionKit.positionProvider.getIpAddress === "function" ) return PositionKit.positionProvider.stateMessage + " - " + PositionKit.positionProvider.getIpAddress() return PositionKit.positionProvider.stateMessage } - return model.ProviderDescription + return listdelegate.model.ProviderDescription } rightContent: MMComponents.MMRoundButton { - visible: model.ProviderType !== "internal" + visible: listdelegate.model.ProviderType !== "internal" iconSource: __style.deleteIcon - onClicked: removeDialog.openDialog( model.ProviderId ) + onClicked: removeDialog.openDialog( listdelegate.model.ProviderId ) } hasLine: { - if ( index === ListView.view.count - 1 ) return false + if ( listdelegate.index === ListView.view.count - 1 ) return false if ( ListView.section === "internal" ) { - let ix = providersModel.index( index + 1, 0 ) + let ix = providersModel.index( listdelegate.index + 1, 0 ) let type = providersModel.data( ix, MM.PositionProvidersModel.ProviderType ) if ( type.includes( "external" ) ) return false } @@ -106,7 +91,7 @@ MMComponents.MMPage { return true } - onClicked: root.constructProvider( model.ProviderType, model.ProviderId, model.ProviderName ) + onClicked: root.activateProvider( listdelegate.model.ProviderType, listdelegate.model.ProviderId, listdelegate.model.ProviderName ) } section { @@ -154,17 +139,17 @@ MMComponents.MMPage { MMProviderTypeDrawer { id: providerTypeDrawer - onBluetoothSelected: bluetoothDiscoveryLoader.active = true - onNetworkSelected: networkProviderDrawer.open() + onProviderSelected: function( providerType ) { + if ( providerType === "bluetooth" ) bluetoothDiscoveryLoader.active = true + else if ( providerType === "network" ) networkProviderDrawer.open() + } } MMNetworkProviderDrawer { id: networkProviderDrawer onConfirmed: function( alias, deviceAddress ) { - PositionKit.positionProvider = PositionKit.constructProvider( "external_ip", deviceAddress, alias ) - providersModel.addProvider( PositionKit.positionProvider.name(), deviceAddress, "external_ip" ) - connectingDialogLoaderNetwork.open() + root.activateProvider( "external_ip", deviceAddress, alias ) } } @@ -200,7 +185,7 @@ MMComponents.MMPage { if ( AppSettings.activePositionProviderId === removeDialog.providerId ) { // we are removing an active provider, replace it with internal provider - root.constructProvider( "internal", "devicegps", qsTr( "Internal" ) ) + root.activateProvider( "internal", "devicegps", qsTr( "Internal" ) ) } providersModel.removeProvider( removeDialog.providerId ) @@ -213,7 +198,7 @@ MMComponents.MMPage { id: bluetoothDiscoveryLoader active: false - source: Qt.resolvedUrl( "MMBluetoothProviderDrawer.qml" ) + sourceComponent: Component { MMBluetoothProviderDrawer {} } onLoaded: item.open() } @@ -222,11 +207,9 @@ MMComponents.MMPage { target: bluetoothDiscoveryLoader.item function onInitiatedConnectionTo( deviceAddress, deviceName ) { - PositionKit.positionProvider = PositionKit.constructProvider( "external_bt", deviceAddress, deviceName ) - providersModel.addProvider( deviceName, deviceAddress, "external_bt" ) bluetoothDiscoveryLoader.item.list.model.discovering = false bluetoothDiscoveryLoader.item.close() - connectingDialogLoader.open() + root.activateProvider( "external_bt", deviceAddress, deviceName ) } function onClosed() { bluetoothDiscoveryLoader.active = false } @@ -235,16 +218,19 @@ MMComponents.MMPage { Loader { id: connectingDialogLoader + property string providerType: "" + active: false asynchronous: true - source: Qt.resolvedUrl( "MMExternalProviderConnectionDrawer.qml" ) + sourceComponent: Component { MMExternalProviderConnectionDrawer{} } onLoaded: { - item.providerType = "bluetooth" + item.providerType = connectingDialogLoader.providerType item.open() } - function open() { + function open( type ) { + providerType = type active = true focus = true } @@ -254,32 +240,7 @@ MMComponents.MMPage { target: connectingDialogLoader.item function onClosed() { connectingDialogLoader.active = false } - function onFailure() { PositionKit.positionProvider = PositionKit.constructProvider( "internal", "devicegps", "" ) } - } - - Loader { - id: connectingDialogLoaderNetwork - - active: false - asynchronous: true - source: Qt.resolvedUrl( "MMExternalProviderConnectionDrawer.qml" ) - - onLoaded: { - item.providerType = "network" - item.open() - } - - function open() { - active = true - focus = true - } - } - - Connections { - target: connectingDialogLoaderNetwork.item - - function onClosed() { connectingDialogLoaderNetwork.active = false } - function onFailure() { PositionKit.positionProvider = PositionKit.constructProvider( "internal", "devicegps", "" ) } + function onFailure() { root.activateProvider( "internal", "devicegps", "" ) } } } @@ -289,7 +250,7 @@ MMComponents.MMPage { property string pageTitle: qsTr( "Manage GPS receivers" ) } - function constructProvider( type, id, name ) { + function activateProvider( type, id, name ) { if ( type === "external_bt" ) { // Is bluetooth turned on? if ( !__inputUtils.isBluetoothTurnedOn() ) { @@ -302,13 +263,14 @@ MMComponents.MMPage { return // do not construct the same provider again } + providersModel.addProvider( name, id, type ) PositionKit.positionProvider = PositionKit.constructProvider( type, id, name ) if ( type === "external_bt" ) { - connectingDialogLoader.open() + connectingDialogLoader.open( "bluetooth" ) } else if ( type === "external_ip" ) { - connectingDialogLoaderNetwork.open() + connectingDialogLoader.open( "network" ) } } } diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index 284b59c04..b03e540f7 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -6,7 +6,7 @@ * (at your option) any later version. * * * ***************************************************************************/ - +pragma ComponentBehavior: Bound import QtQuick import QtQml.Models import "../components" as MMComponents @@ -14,15 +14,12 @@ import "../components" as MMComponents MMComponents.MMListDrawer { id: root - property string selectedProviderType: "" - - signal bluetoothSelected() - signal networkSelected() + signal providerSelected( string providerType ) drawerHeader.title: qsTr( "Connect new receiver" ) drawerHeader.titleFont: __style.t2 - onOpened: root.selectedProviderType = "" + onOpened: root.list.currentIndex = -1 list.model: ListModel { id: providerTypeModel @@ -36,49 +33,54 @@ MMComponents.MMListDrawer { } list.header: MMComponents.MMText { - width: ListView.view.width - - text: __inputUtils.htmlLink( - qsTr( "External receivers use different connection methods depending on the manufacturer. Select a connection type below, or %1check our documentation%2 for supported devices and setup instructions." ), - __style.nightColor, - __inputHelp.howToConnectGPSLink, - "", - true, - false - ) - font: __style.p5 - color: __style.nightColor - textFormat: Text.RichText - - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignJustify - bottomPadding: __style.margin16 - - onLinkActivated: function( link ) { - Qt.openUrlExternally( link ) - } + width: ListView.view.width + text: __inputUtils.htmlLink( + qsTr( "External receivers use different connection methods depending on the manufacturer. Select a connection type below, or %1check our documentation%2 for supported devices and setup instructions." ), + __style.nightColor, + __inputHelp.howToConnectGPSLink, + "", + true, + false + ) + font: __style.p5 + color: __style.nightColor + textFormat: Text.RichText + + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignJustify + bottomPadding: __style.margin16 + + onLinkActivated: function( link ) { + Qt.openUrlExternally( link ) } + } list.delegate: Item { + required property string name + required property string description + required property string type + required property url icon + required property int index + width: ListView.view.width height: checkbox.height + __style.margin12 - MMComponents.MMIconCheckBoxHorizontal{ + MMComponents.MMIconCheckBoxHorizontal { id: checkbox width: parent.width showBorder: true - sourceIcon: model.icon - text: model.name - description: model.description - checked: root.selectedProviderType === model.type + sourceIcon: parent.icon + text: parent.name + description: parent.description + checked: parent.ListView.isCurrentItem onClicked: { - if ( root.selectedProviderType === model.type ) { - root.selectedProviderType = "" + if ( parent.ListView.isCurrentItem ) { + parent.ListView.view.currentIndex = -1 } else { - root.selectedProviderType = model.type + parent.ListView.view.currentIndex = parent.index } } } @@ -96,16 +98,17 @@ MMComponents.MMListDrawer { anchors.topMargin: __style.margin8 text: qsTr( "Continue" ) - enabled: root.selectedProviderType !== "" + enabled: root.list.currentIndex !== -1 onClicked: { + const providerType = providerTypeModel.get( root.list.currentIndex ).type root.close() - if ( root.selectedProviderType === "bluetooth" ) { - root.bluetoothSelected() + if ( providerType === "bluetooth" ) { + root.providerSelected("bluetooth") } - else if ( root.selectedProviderType === "network" ) { - root.networkSelected() + else if ( providerType === "network" ) { + root.providerSelected("network") } } } diff --git a/gallery/qml/pages/ChecksPage.qml b/gallery/qml/pages/ChecksPage.qml index 8f33550ad..7d14690c8 100644 --- a/gallery/qml/pages/ChecksPage.qml +++ b/gallery/qml/pages/ChecksPage.qml @@ -100,6 +100,17 @@ ScrollView { description: "This is a small description to check the functionality of this component" small: false } + + + MMComponents.MMIconCheckBoxHorizontal{ + checked: false + width: 300 * __dp + sourceIcon: __style.redditIcon + text: "Internet provider mock" + description: "This is a small description to check the functionality of this component" + small: false + showBorder: true + } } } From 3900d27564c93d3aab7510a9fe20668c134d80a3 Mon Sep 17 00:00:00 2001 From: Gabriel Bolbotina Date: Fri, 8 May 2026 09:47:36 +0300 Subject: [PATCH 30/30] Simplified QML according to the review findings --- app/qml/gps/MMGpsDataDrawer.qml | 4 +-- app/qml/gps/MMNetworkProviderDrawer.qml | 2 +- app/qml/gps/MMProviderTypeDrawer.qml | 33 +++++++++---------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/app/qml/gps/MMGpsDataDrawer.qml b/app/qml/gps/MMGpsDataDrawer.qml index 0d32e3930..b375b29cd 100644 --- a/app/qml/gps/MMGpsDataDrawer.qml +++ b/app/qml/gps/MMGpsDataDrawer.qml @@ -351,7 +351,7 @@ MMComponents.MMDrawer { anchors { bottom: parent.bottom - bottomMargin: __style.safeAreaBottom + __style.margin8 + bottomMargin: 0 } text: qsTr( "Manage GPS receivers" ) @@ -370,6 +370,6 @@ MMComponents.MMDrawer { id: internal property string coordinatesInDegrees: __inputUtils.degreesString( PositionKit.positionCoordinate ) - readonly property real reservedButtonSpace: __style.spacing20 + manageButton.implicitHeight + __style.safeAreaBottom + __style.margin8 + readonly property real reservedButtonSpace: __style.spacing20 + manageButton.implicitHeight + __style.margin8 } } diff --git a/app/qml/gps/MMNetworkProviderDrawer.qml b/app/qml/gps/MMNetworkProviderDrawer.qml index 0a7090d48..058766f7d 100644 --- a/app/qml/gps/MMNetworkProviderDrawer.qml +++ b/app/qml/gps/MMNetworkProviderDrawer.qml @@ -96,7 +96,7 @@ MMComponents.MMDrawer { if ( !port ) { portInput.errorMsg = qsTr( "Port is required" ) } - else if ( !/^\d+$/.test( port ) || portNum < 1 || portNum > 65535 ) { + else if ( portNum < 1 || portNum > 65535 ) { portInput.errorMsg = qsTr( "Enter a valid port (1–65535)" ) } else { diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index b03e540f7..d08181e1b 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -86,30 +86,21 @@ MMComponents.MMListDrawer { } } - list.footer: Item { - width: ListView.view.width - height: continueButton.height + __style.margin20 - - MMComponents.MMButton { - id: continueButton - - width: parent.width - anchors.top: parent.top - anchors.topMargin: __style.margin8 + list.footer: MMComponents.MMButton { + width: parent.width - text: qsTr( "Continue" ) - enabled: root.list.currentIndex !== -1 + text: qsTr( "Continue" ) + enabled: root.list.currentIndex !== -1 - onClicked: { - const providerType = providerTypeModel.get( root.list.currentIndex ).type - root.close() + onClicked: { + const providerType = providerTypeModel.get( root.list.currentIndex ).type + root.close() - if ( providerType === "bluetooth" ) { - root.providerSelected("bluetooth") - } - else if ( providerType === "network" ) { - root.providerSelected("network") - } + if ( providerType === "bluetooth" ) { + root.providerSelected("bluetooth") + } + else if ( providerType === "network" ) { + root.providerSelected("network") } } }