diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4d859f3ec..78c2fac96 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -112,6 +112,18 @@ jobs: -k "$MERGINSECRETS_DECRYPT_KEY" \ -md md5 + - name: Extract Trimble App ID + env: + TRIMBLE_SECRETS_DECRYPT_KEY: ${{ secrets.TRIMBLE_SECRETS_DECRYPT_KEY }} + run: | + cd mm/app/position/providers/ + $HOMEBREW_PREFIX/bin/openssl \ + aes-256-cbc -d \ + -in trimblesecrets.cpp.enc \ + -out trimblesecrets.cpp \ + -k "$TRIMBLE_SECRETS_DECRYPT_KEY" \ + -md md5 + - name: ccache uses: hendrikmuhs/ccache-action@v1.2 with: @@ -237,6 +249,7 @@ jobs: -DQT_ANDROID_SIGN_APK=Yes \ -DQT_ANDROID_SIGN_AAB=Yes \ -DUSE_MM_SERVER_API_KEY=Yes \ + -DWITH_TRIMBLE_PROVIDERS=TRUE \ -DUSE_KEYCHAIN=No \ -DCMAKE_TOOLCHAIN_FILE:PATH="${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" \ -GNinja \ diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 5eb881dd3..5e5359387 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -107,6 +107,18 @@ jobs: -k "$MERGINSECRETS_DECRYPT_KEY" \ -md md5 + - name: Extract Trimble App ID + env: + TRIMBLE_SECRETS_DECRYPT_KEY: ${{ secrets.TRIMBLE_SECRETS_DECRYPT_KEY }} + run: | + cd mm/app/position/providers/ + $HOMEBREW_PREFIX/bin/openssl \ + aes-256-cbc -d \ + -in trimblesecrets.cpp.enc \ + -out trimblesecrets.cpp \ + -k "$TRIMBLE_SECRETS_DECRYPT_KEY" \ + -md md5 + - name: Configure Keychain run: | security create-keychain -p "" "$KEYCHAIN" @@ -164,6 +176,7 @@ jobs: -D CMAKE_SYSTEM_PROCESSOR=aarch64 \ -DIOS=TRUE \ -DUSE_MM_SERVER_API_KEY=TRUE \ + -DWITH_TRIMBLE_PROVIDERS=TRUE \ -DUSE_KEYCHAIN=No \ -DCMAKE_INSTALL_PREFIX:PATH=../install-mm \ -G "Xcode" \ diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3417ab130..1f6053911 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -119,7 +119,7 @@ jobs: -DVCPKG_TARGET_TRIPLET=${{ env.TRIPLET }} \ -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake \ -DUSE_MM_SERVER_API_KEY=TRUE \ - -DHAVE_BLUETOOTH=FALSE \ + -DWITH_BLUETOOTH_PROVIDERS=FALSE \ -DUSE_KEYCHAIN=No \ -DCOVERAGE=TRUE \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ diff --git a/.gitignore b/.gitignore index 272a0a1b4..18ddc4b27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,30 @@ +# broad file type filters *.autosave *.orig *.DS_Store *.user -*build*/ -app/config.pri -core/merginsecrets.cpp *.idea *.vscode *.cache -test/temp_projects/ -test/temp_extra_projects/ +*.gpkg-wal +*.gpkg-shm +# secrets +core/merginsecrets.cpp +app/position/providers/trimblesecrets.cpp +.github/secrets/ios/LutraConsulting*.mobileprovision +Input_keystore.keystore +google_play_key.json +# generated files +app/config.pri +*build*/ input.pro.user* app/android/assets app/android/AndroidManifest.xml app/android/build.gradle app/android/.gradle -*.gpkg-wal -*.gpkg-shm -Input_keystore.keystore -CMakeLists.txt.user -.github/secrets/ios/LutraConsulting*.mobileprovision -google_play_key.json +test/temp_projects/ +test/temp_extra_projects/ fastlane/report.xml -CMakeUserPresets.json \ No newline at end of file +CMakeLists.txt.user +CMakeUserPresets.json +CLAUDE.md diff --git a/CMakeLists.txt b/CMakeLists.txt index c9ab2ff00..105548ecd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,9 +81,9 @@ else () endif () if (IOS) - set(HAVE_BLUETOOTH_DEFAULT FALSE) + set(WITH_BLUETOOTH_PROVIDERS_DEFAULT FALSE) else () - set(HAVE_BLUETOOTH_DEFAULT TRUE) + set(WITH_BLUETOOTH_PROVIDERS_DEFAULT TRUE) endif () if (DEFINED ENV{MM_VERSION_CODE}) @@ -138,11 +138,18 @@ set(ENABLE_TESTS ${ENABLE_TESTS_DEFAULT} CACHE BOOL "Whether to build tests" ) -set(HAVE_BLUETOOTH - ${HAVE_BLUETOOTH_DEFAULT} +set(WITH_BLUETOOTH_PROVIDERS + ${WITH_BLUETOOTH_PROVIDERS_DEFAULT} CACHE BOOL "Building with bluetooth position provider" ) +set(WITH_TRIMBLE_PROVIDERS + FALSE + CACHE + BOOL + "Building with Trimble GNSS position provider via Trimble Mobile Manager (mobile only)" +) + set(USE_KEYCHAIN FALSE CACHE @@ -257,7 +264,7 @@ if (IOS OR MACOS) find_package(Tasn1 REQUIRED) endif () -if (HAVE_BLUETOOTH) +if (WITH_BLUETOOTH_PROVIDERS) find_package( Qt6 COMPONENTS Bluetooth @@ -265,6 +272,21 @@ if (HAVE_BLUETOOTH) ) endif () +if (WITH_TRIMBLE_PROVIDERS) + if (NOT ANDROID AND NOT IOS) + message( + FATAL_ERROR + "WITH_TRIMBLE_PROVIDERS is only supported on Android and iOS. Set WITH_TRIMBLE_PROVIDERS=FALSE for desktop builds." + ) + else () + find_package( + Qt6 + COMPONENTS WebSockets + REQUIRED + ) + endif () +endif () + if (ENABLE_TESTS) find_package( Qt6 diff --git a/INSTALL.md b/INSTALL.md index 8095cdee3..76259ca11 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -60,6 +60,7 @@ Open workflow file for your platform/target and see the version of libraries use ## 2.1 Secrets +### Mergin API To communicate with MerginAPI, some endpoints need to attach `api_key`. To not leak API_KEY, the source code that returns the API_KEYS is encrypted. @@ -76,17 +77,34 @@ manifest files. encrypt -``` +```bash cd core/ openssl aes-256-cbc -in merginsecrets.cpp -out merginsecrets.cpp.enc -md md5 ``` decrypt -``` +```bash cd core/ openssl aes-256-cbc -d -in merginsecrets.cpp.enc -out merginsecrets.cpp -md md5 ``` +### Trimble +Similar approach has been used with APP ID for Trimble Mobile Manager. Having access to this file shouldn't be necessary. +You can always defer the build to CI. + +encrypt + +```bash +cd app/position/providers +openssl aes-256-cbc -in trimblesecrets.cpp -out trimblesecrets.cpp.enc -md md5 +``` + +decrypt + +```bash +cd app/position/providers +openssl aes-256-cbc -d -in trimblesecrets.cpp.enc -out trimblesecrets.cpp -md md5 +``` ## 2.2 Code formatting @@ -109,7 +127,7 @@ to install QtCreator and Qt on your host to be able to release translations. Dependencies are build with vcpkg. To fix the version of libraries, you need to download vcpkg and checkout to git commit specified in the file `VCPKG_BASELINE` in the repository. The VCPKG repository **HAS TO BE** outside the mobile repository. -``` +```bash git clone https://github.com/microsoft/vcpkg.git VCPKG_TAG=`cat mobile/VCPKG_BASELINE` git checkout ${VCPKG_TAG} @@ -130,7 +148,7 @@ Steps to build and run mobile app: 1. Install some dependencies, critically bison and flex. See "Install Build Dependencies" step in `.github/workflows/linux.yml` - ``` + ```bash sudo apt-get install -y \ gperf autopoint '^libxcb.*-dev' libx11-xcb-dev libegl1-mesa-dev \ libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev \ @@ -163,7 +181,7 @@ Steps to build and run mobile app: To use USE_MM_SERVER_API_KEY read [Secrets](#secrets) section. - ``` + ```bash mkdir -p build cd build cmake \ @@ -182,13 +200,13 @@ Steps to build and run mobile app: 4. Build application - ``` + ```bash ninja ``` 5. Run mobile app - ``` + ```bash ./app/MerginMaps ``` @@ -227,7 +245,7 @@ For building ABIs see https://www.qt.io/blog/android-multi-abi-builds-are-back can take considerable time (e.g. an hour). Subsequent runs will be faster as the libraries without change will be taken from local binary vcpkg cache. - ``` + ```bash export ANDROID_NDK_HOME=/home//android/ndk/ export ANDROID_SDK_ROOT=/home//android export QT_ANDROID_KEYSTORE_ALIAS= @@ -287,7 +305,7 @@ To use USE_MM_SERVER_API_KEY read [Secrets](#secrets) section. To build the project, go to the build folder and run the following command: -``` +```bash ninja ``` @@ -343,7 +361,7 @@ build_folder/ can take considerable time (e.g. an hour). Subsequent runs will be faster as the libraries without change will be taken from local binary vcpkg cache. - ``` + ```bash export ANDROID_NDK_HOME=/Users//android/ndk/ export ANDROID_SDK_ROOT=/Users//android export QT_ANDROID_KEYSTORE_ALIAS= @@ -406,13 +424,13 @@ build_folder/ To build the project, go to the build folder and run the following command: - ``` + ```bash ninja ``` Once built, navigate to the path and run MerginMaps: - ``` + ```bash build_folder/ app/ MerginMaps @@ -457,7 +475,7 @@ mobile app for Android on Windows, please help us to update this section. Note: make sure you adjust VCPKG_HOST_TRIPLET and CMAKE_SYSTEM_PROCESSOR if you use x64-osx host machine. - ``` + ```bash cd build export PATH=$(brew --prefix flex)/bin:$(brew --prefix bison)/bin:$(brew --prefix gettext)/bin:$PATH;\ @@ -469,14 +487,14 @@ mobile app for Android on Windows, please help us to update this section. -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ -DVCPKG_TARGET_TRIPLET=arm64-ios \ -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake \ - -D ENABLE_BITCODE=OFF \ - -D ENABLE_ARC=ON \ - -D CMAKE_CXX_VISIBILITY_PRESET=hidden \ - -D CMAKE_SYSTEM_NAME=iOS \ + -DENABLE_BITCODE=OFF \ + -DENABLE_ARC=ON \ + -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ + -DCMAKE_SYSTEM_NAME=iOS \ -DIOS=TRUE \ -DUSE_MM_SERVER_API_KEY=FALSE \ - -G "Xcode" \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -G "Xcode" \ -S ../mobile \ -B ./ ``` @@ -485,7 +503,7 @@ mobile app for Android on Windows, please help us to update this section. Now you can create a build (either on command line or by setting these variables in Qt Creator) -``` +```bash cd build xcodebuild \ @@ -509,7 +527,7 @@ Once the project is opened, build it from Xcode. 1. Install some dependencies, critically XCode, bison and flex. See "Install Build Dependencies" step in `.github/workflows/macos.yml` -``` +```bash brew install cmake automake bison flex gnu-sed autoconf-archive libtool ninja pkg-config ``` @@ -534,7 +552,7 @@ Once the project is opened, build it from Xcode. Note: for **x64-osx** (intel laptops) build use **x64-osx** VCPKG_TARGET_TRIPLET instead of **arm64-osx** (Mx laptops) - ``` + ```bash cd build export PATH=$(brew --prefix flex)/bin:$(brew --prefix bison)/bin:$(brew --prefix gettext)/bin:$PATH;\ @@ -555,12 +573,12 @@ Once the project is opened, build it from Xcode. 4. Build application - ``` + ```bash ninja ``` 5. Run the mobile app - ``` + ```bash ./app/MerginMaps.app/Contents/MacOS/MerginMaps ``` @@ -569,7 +587,7 @@ Once the project is opened, build it from Xcode. 1. Install some dependencies. See `.github/workflows/win.yml` Critically Visual Studio, cmake, bison and flex. Setup build VS environment (adjust to your version) -``` +```shell "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" -arch=x64 ``` @@ -595,31 +613,31 @@ Once the project is opened, build it from Xcode. To use USE_MM_SERVER_API_KEY read [Secrets](#secrets) section. - ``` + ```shell mkdir build cd build cmake ^ -DCMAKE_BUILD_TYPE=Debug ^ -DCMAKE_TOOLCHAIN_FILE:PATH="/vcpkg/scripts/buildsystems/vcpkg.cmake" ^ - -G "Visual Studio 17 2022" ^ - -A x64 ^ -DVCPKG_TARGET_TRIPLET=x64-windows ^ -DUSE_MM_SERVER_API_KEY=FALSE ^ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ^ + -A x64 ^ + -G "Visual Studio 17 2022" ^ -S ../mobile ^ -B . ``` 4. Build application - ``` + ```shell cd build cmake --build . --config Release --verbose ``` 5. Run the mobile app - ``` + ```shell ./app/MerginMaps.exe ``` diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index a4ec70413..ed910b431 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -203,12 +203,32 @@ if (NOT WIN32) set(MM_SRCS ${MM_SRCS} static_plugins.cpp) endif () -if (HAVE_BLUETOOTH) +if (WITH_BLUETOOTH_PROVIDERS) set(MM_SRCS ${MM_SRCS} position/providers/bluetoothpositionprovider.cpp) set(MM_HDRS ${MM_HDRS} position/providers/bluetoothpositionprovider.h) endif () +if (WITH_TRIMBLE_PROVIDERS) + if (NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/position/providers/trimblesecrets.cpp) + message( + FATAL_ERROR + "app/position/providers/trimblesecrets.cpp doesn't exist. Decrypt trimblesecrets.cpp.enc or set WITH_TRIMBLE_PROVIDERS=FALSE" + ) + endif () + + set(MM_HDRS ${MM_HDRS} position/providers/trimbleregistration.h + position/providers/trimblepositionprovider.h + ) + set(MM_SRCS ${MM_SRCS} position/providers/trimblepositionprovider.cpp) + + if (ANDROID) + set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistrationandroid.cpp) + elseif (IOS) + set(MM_SRCS ${MM_SRCS} position/providers/trimbleregistrationios.mm) + endif () +endif () + if (ENABLE_TESTS) set(MM_SRCS ${MM_SRCS} @@ -329,6 +349,12 @@ qt_add_executable( main.cpp ) +# throw error when #include is missing, guards against not imported preprocessor +# definitions +if (CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|GNU)$") + target_compile_options(MerginMaps PRIVATE -Werror=undef) +endif () + set_target_properties(MerginMaps PROPERTIES WIN32_EXECUTABLE TRUE) target_include_directories( @@ -513,10 +539,14 @@ if (ANDROID) target_link_libraries(MerginMaps PRIVATE Qt6::CorePrivate) endif () -if (HAVE_BLUETOOTH) +if (WITH_BLUETOOTH_PROVIDERS) target_link_libraries(MerginMaps PUBLIC Qt6::Bluetooth) endif () +if (WITH_TRIMBLE_PROVIDERS) + target_link_libraries(MerginMaps PUBLIC Qt6::WebSockets) +endif () + if (NOT IOS) target_link_libraries(MerginMaps PUBLIC Qt6::PrintSupport) endif () diff --git a/app/bluetoothdiscoverymodel.cpp b/app/bluetoothdiscoverymodel.cpp index 2a3107be4..808f68c22 100644 --- a/app/bluetoothdiscoverymodel.cpp +++ b/app/bluetoothdiscoverymodel.cpp @@ -10,14 +10,14 @@ #include "bluetoothdiscoverymodel.h" #include "coreutils.h" -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS #include #endif BluetoothDiscoveryModel::BluetoothDiscoveryModel( QObject *parent ) : QAbstractListModel( parent ) { -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS mDiscoveryAgent = std::unique_ptr( new QBluetoothDeviceDiscoveryAgent() ); connect( mDiscoveryAgent.get(), &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDiscoveryModel::deviceDiscovered ); @@ -39,7 +39,7 @@ QHash BluetoothDiscoveryModel::roleNames() const { QHash roles; -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS roles.insert( DataRoles::DeviceAddress, "deviceAddress" ); roles.insert( DataRoles::DeviceName, "deviceName" ); roles.insert( DataRoles::SignalStrength, "signalStrength" ); @@ -50,7 +50,7 @@ QHash BluetoothDiscoveryModel::roleNames() const int BluetoothDiscoveryModel::rowCount( const QModelIndex & ) const { -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS return mFoundDevices.count(); #else return 0; @@ -59,7 +59,7 @@ int BluetoothDiscoveryModel::rowCount( const QModelIndex & ) const QVariant BluetoothDiscoveryModel::data( const QModelIndex &index, int role ) const { -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS if ( !index.isValid() ) return QVariant(); @@ -104,7 +104,7 @@ void BluetoothDiscoveryModel::setDiscovering( bool discovering ) if ( mDiscovering == discovering ) return; -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS if ( discovering ) { mDiscoveryAgent->start(); @@ -120,7 +120,7 @@ void BluetoothDiscoveryModel::setDiscovering( bool discovering ) emit discoveringChanged( mDiscovering ); } -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS void BluetoothDiscoveryModel::deviceDiscovered( const QBluetoothDeviceInfo &device ) { for ( int i = 0; i < mFoundDevices.count(); i++ ) diff --git a/app/bluetoothdiscoverymodel.h b/app/bluetoothdiscoverymodel.h index c9b35dd28..90f01502c 100644 --- a/app/bluetoothdiscoverymodel.h +++ b/app/bluetoothdiscoverymodel.h @@ -17,7 +17,7 @@ #include "mmconfig.h" -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS #include #include #endif @@ -50,7 +50,7 @@ class BluetoothDiscoveryModel : public QAbstractListModel public slots: -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS void deviceDiscovered( const QBluetoothDeviceInfo &info ); void deviceUpdated( const QBluetoothDeviceInfo &info, QBluetoothDeviceInfo::Fields updatedFields ); #endif @@ -62,7 +62,7 @@ class BluetoothDiscoveryModel : public QAbstractListModel private: bool mDiscovering = false; -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS QList mFoundDevices; std::unique_ptr mDiscoveryAgent; #endif diff --git a/app/main.cpp b/app/main.cpp index 3057bd5da..d3aa6ded1 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -759,12 +759,6 @@ int main( int argc, char *argv[] ) engine.rootContext()->setContextProperty( "__layerDetailLegendImageProvider", layerDetailLegendImageProvider ); engine.addImageProvider( QLatin1String( "LayerDetailLegendImageProvider" ), layerDetailLegendImageProvider ); -#ifdef HAVE_BLUETOOTH - engine.rootContext()->setContextProperty( "__haveBluetooth", true ); -#else - engine.rootContext()->setContextProperty( "__haveBluetooth", false ); -#endif - // Even though enabling QT's HighDPI scaling removes the need to multiply pixel values with dp, // there are screens that need a "little help", because system DPR has different value than the // one we calculated. In these scenarios we use a ratio between real (our) DPR and DPR reported by QT. diff --git a/app/maptools/recordingmaptool.cpp b/app/maptools/recordingmaptool.cpp index 47f16c748..4c69e06df 100644 --- a/app/maptools/recordingmaptool.cpp +++ b/app/maptools/recordingmaptool.cpp @@ -79,8 +79,7 @@ void RecordingMapTool::addPoint( const QgsPoint &point ) fixZM( pointToAdd ); - // apply gps antenna height - if ( QgsWkbTypes::hasZ( pointToAdd.wkbType() ) && mPositionKit && mPositionKit->antennaHeight() > 0 ) + if ( QgsWkbTypes::hasZ( pointToAdd.wkbType() ) && mPositionKit && mPositionKit->requireAntennaHeightTransform() ) { pointToAdd.setZ( pointToAdd.z() - mPositionKit->antennaHeight() ); } diff --git a/app/position/geoposition.cpp b/app/position/geoposition.cpp index c955cb669..f96518626 100644 --- a/app/position/geoposition.cpp +++ b/app/position/geoposition.cpp @@ -131,32 +131,30 @@ QString GeoPosition::parseFixStatus() const // 9 = WAAS fix (not NMEA standard, but NovAtel receivers report this instead of a 2). // - switch ( quality ) + switch ( qualityIndicator ) { - case -1: + case Qgis::GpsQualityIndicator::Unknown: return QObject::tr( "No data" ); - case 0: + case Qgis::GpsQualityIndicator::Invalid: return QObject::tr( "No fix" ); - case 1: + case Qgis::GpsQualityIndicator::GPS: return QObject::tr( "GPS fix, no correction data" ); - case 2: - // fall through - case 9: + case Qgis::GpsQualityIndicator::DGPS: return QObject::tr( "DGPS fix" ); - case 3: + case Qgis::GpsQualityIndicator::PPS: return QObject::tr( "PPS fix" ); - case 4: + case Qgis::GpsQualityIndicator::RTK: return QObject::tr( "RTK fix" ); - case 5: + case Qgis::GpsQualityIndicator::FloatRTK: return QObject::tr( "RTK float" ); - case 6: + case Qgis::GpsQualityIndicator::Estimated: return QObject::tr( "Estimated fix (dead reckoning)" ); default: diff --git a/app/position/positionkit.cpp b/app/position/positionkit.cpp index 04c086b14..cdac5bf03 100644 --- a/app/position/positionkit.cpp +++ b/app/position/positionkit.cpp @@ -16,12 +16,15 @@ #include "appsettings.h" #include "inpututils.h" -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS #include "providers/bluetoothpositionprovider.h" #endif #include "providers/internalpositionprovider.h" #include "providers/simulatedpositionprovider.h" #include "providers/networkpositionprovider.h" +#if WITH_TRIMBLE_PROVIDERS +#include "providers/trimblepositionprovider.h" +#endif #ifdef ANDROID #include "providers/androidpositionprovider.h" #include @@ -55,6 +58,11 @@ QString PositionKit::positionCrs3DGeoidModelName() return QgsCoordinateReferenceSystem::fromEpsgId( 5773 ).description(); } + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + return dynamic_cast( mPositionProvider.get() )->geoidModelName(); + } + return {}; } @@ -99,7 +107,6 @@ void PositionKit::setPositionProvider( AbstractPositionProvider *provider ) if ( mPositionProvider ) { connect( mPositionProvider.get(), &AbstractPositionProvider::positionChanged, this, &PositionKit::parsePositionUpdate ); - CoreUtils::log( QStringLiteral( "PositionKit" ), QStringLiteral( "Changed position provider to: %1" ).arg( provider->id() ) ); } else // passed nullptr @@ -126,7 +133,7 @@ QString PositionKit::positionProviderName() const AbstractPositionProvider *PositionKit::constructProvider( const QString &type, const QString &id, const QString &name ) { -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS if ( type == QStringLiteral( "external_bt" ) ) { AbstractPositionProvider *provider = new BluetoothPositionProvider( id, name, *mPositionTransformer ); @@ -135,6 +142,15 @@ AbstractPositionProvider *PositionKit::constructProvider( const QString &type, c } #endif +#if WITH_TRIMBLE_PROVIDERS + if ( type == QStringLiteral( "external_trimble" ) ) + { + AbstractPositionProvider *provider = new TrimblePositionProvider( id, name, *mPositionTransformer ); + QQmlEngine::setObjectOwnership( provider, QQmlEngine::CppOwnership ); + return provider; + } +#endif + if ( type == QStringLiteral( "external_ip" ) ) { QString providerName( name ); @@ -243,6 +259,24 @@ AbstractPositionProvider *PositionKit::constructActiveProvider( const AppSetting return constructProvider( providerType, providerId, providerName ); } +bool PositionKit::hasTrimbleSupport() +{ +#if WITH_TRIMBLE_PROVIDERS + return true; +#else + return false; +#endif +} + +bool PositionKit::hasBluetoothSupport() +{ +#if WITH_BLUETOOTH_PROVIDERS + return true; +#else + return false; +#endif +} + void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) { bool hasAnythingChanged = false; @@ -263,9 +297,9 @@ void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) hasAnythingChanged = true; } - if ( !qgsDoubleNear( newPosition.elevation - antennaHeight(), mPosition.elevation ) ) + if ( !qgsDoubleNear( newPosition.elevation - ( requireAntennaHeightTransform() ? 0 : antennaHeight() ), mPosition.elevation ) ) { - mPosition.elevation = newPosition.elevation - antennaHeight(); + mPosition.elevation = newPosition.elevation - ( requireAntennaHeightTransform() ? 0 : antennaHeight() ); emit altitudeChanged( mPosition.elevation ); hasAnythingChanged = true; } @@ -368,9 +402,9 @@ void PositionKit::parsePositionUpdate( const GeoPosition &newPosition ) hasAnythingChanged = true; } - if ( newPosition.quality != mPosition.quality ) + if ( newPosition.qualityIndicator != mPosition.qualityIndicator ) { - mPosition.quality = newPosition.quality; + mPosition.qualityIndicator = newPosition.qualityIndicator; hasAnythingChanged = true; } @@ -434,7 +468,15 @@ void PositionKit::appStateChanged( const Qt::ApplicationState state ) void PositionKit::refreshPositionTransformer( const QgsCoordinateTransformContext &transformContext ) { - const QgsCoordinateReferenceSystem srcCrs = positionCrs3DEllipsoidHeight(); + QgsCoordinateReferenceSystem srcCrs; + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + srcCrs = dynamic_cast( mPositionProvider.get() )->sourceCrs(); + } + else + { + srcCrs = positionCrs3DEllipsoidHeight(); + } const QgsCoordinateReferenceSystem destCrs = positionCrs3D(); QgsCoordinateTransformContext context = transformContext; @@ -604,10 +646,37 @@ void PositionKit::setAppSettings( AppSettings *appSettings ) double PositionKit::antennaHeight() const { - if ( mAppSettings ) + // trimble provider has antenna height defined in TMM, so we "block" application setup + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) { - return mAppSettings->gpsAntennaHeight(); +#if WITH_TRIMBLE_PROVIDERS + return dynamic_cast( mPositionProvider.get() )->antennaHeight(); +#else + return 0; +#endif } + if ( mAppSettings ) + return mAppSettings->gpsAntennaHeight(); + return 0; } + +bool PositionKit::requireAntennaHeightTransform() const +{ + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + return false; + } + return true; +} + +void PositionKit::openAntennaHeightPage() const +{ +#if WITH_TRIMBLE_PROVIDERS + if ( mPositionProvider->type() == QStringLiteral( "external_trimble" ) ) + { + dynamic_cast( mPositionProvider.get() )->openAntennaHeightPage(); + } +#endif +} diff --git a/app/position/positionkit.h b/app/position/positionkit.h index 4feee113c..b5d8a9d97 100644 --- a/app/position/positionkit.h +++ b/app/position/positionkit.h @@ -79,6 +79,8 @@ class PositionKit : public QObject Q_PROPERTY( AbstractPositionProvider *positionProvider READ positionProvider WRITE setPositionProvider NOTIFY positionProviderChanged ) Q_PROPERTY( QString positionProviderName READ positionProviderName NOTIFY positionProviderNameChanged ) Q_PROPERTY( bool isMockPosition READ isMockPosition NOTIFY isMockPositionChanged ) + Q_PROPERTY( bool hasTrimbleSupport READ hasTrimbleSupport NOTIFY hasTrimbleSupportChanged ) + Q_PROPERTY( bool hasBluetoothSupport READ hasBluetoothSupport NOTIFY hasBluetoothSupportChanged ) Q_PROPERTY( AppSettings *appSettings READ appSettings WRITE setAppSettings NOTIFY appSettingsChanged ) Q_PROPERTY( double antennaHeight READ antennaHeight NOTIFY antennaHeightChanged ) @@ -138,11 +140,22 @@ class PositionKit : public QObject Q_INVOKABLE AbstractPositionProvider *constructProvider( const QString &type, const QString &id, const QString &name = QString() ); Q_INVOKABLE AbstractPositionProvider *constructActiveProvider( const AppSettings *appsettings ); + static bool hasTrimbleSupport(); + static bool hasBluetoothSupport(); AppSettings *appSettings() const; void setAppSettings( AppSettings *appSettings ); double antennaHeight() const; + /* + * Trimble provider subtracts antenna height before providing the elevation for MM. For every other provider + * subtract antenna height in MM. + */ + Q_INVOKABLE bool requireAntennaHeightTransform() const; + /* + * Opens antenna height setting in Trimble Mobile Manager if using trimble position provider. + */ + Q_INVOKABLE void openAntennaHeightPage() const; void setVerticalCrs( const QgsCoordinateReferenceSystem &verticalCrs ); void setElevationTransformationEnabled( bool elevationTransformationEnabled ); @@ -178,6 +191,8 @@ class PositionKit : public QObject void positionProviderChanged( AbstractPositionProvider *provider ); void positionProviderNameChanged(); + void hasTrimbleSupportChanged(); + void hasBluetoothSupportChanged(); void positionChanged( const GeoPosition & ); void isMockPositionChanged( bool ); diff --git a/app/position/positiontransformer.cpp b/app/position/positiontransformer.cpp index 1b108d996..d44d07be5 100644 --- a/app/position/positiontransformer.cpp +++ b/app/position/positiontransformer.cpp @@ -55,6 +55,11 @@ GeoPosition PositionTransformer::processNetworkPosition( const GeoPosition &geoP return processBluetoothPosition( geoPosition ); } +GeoPosition PositionTransformer::processTrimblePosition( 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 1608dba28..4b7f3fcae 100644 --- a/app/position/positiontransformer.h +++ b/app/position/positiontransformer.h @@ -57,6 +57,14 @@ class PositionTransformer : QObject */ GeoPosition processNetworkPosition( const 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 TrimblePositionProvider to mitigate unnecessary transformations + * \return Copy of passed geoPosition with processed elevation and elevation separation. + */ + GeoPosition processTrimblePosition( 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/abstractpositionprovider.h b/app/position/providers/abstractpositionprovider.h index 03424eae6..22d106e8e 100644 --- a/app/position/providers/abstractpositionprovider.h +++ b/app/position/providers/abstractpositionprovider.h @@ -63,6 +63,14 @@ class AbstractPositionProvider : public QObject void setState( const QString &message ); // keeps state enum the same and only changes the message void setState( const QString &message, State state ); + // signalizes in how many [ms] we will try to reconnect to GPS again + enum ReconnectDelay + { + ShortDelay = 3000, // 3 secs + LongDelay = 5000, // 5 secs + ExtraLongDelay = 10000 // 10 secs + }; + // ProviderId - unique id of this provider. // For external receiver it holds mac address of a bluetooth device. // Internal providers (internal gps and simulated provider) has constant values of "devicegps" and "simulated" diff --git a/app/position/providers/bluetoothpositionprovider.h b/app/position/providers/bluetoothpositionprovider.h index 8797c8ec1..87db1b407 100644 --- a/app/position/providers/bluetoothpositionprovider.h +++ b/app/position/providers/bluetoothpositionprovider.h @@ -26,13 +26,6 @@ class BluetoothPositionProvider : public AbstractPositionProvider { Q_OBJECT - // signalizes in how many [ms] we will try to reconnect to GPS again - enum ReconnectDelay - { - ShortDelay = 3000, - LongDelay = 5000 - }; - public: BluetoothPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); ~BluetoothPositionProvider() override; diff --git a/app/position/providers/networkpositionprovider.h b/app/position/providers/networkpositionprovider.h index cbbd1b0a5..7c5b6384d 100644 --- a/app/position/providers/networkpositionprovider.h +++ b/app/position/providers/networkpositionprovider.h @@ -23,14 +23,6 @@ class NetworkPositionProvider : public AbstractPositionProvider { Q_OBJECT - // signalizes in how many [ms] we will try to reconnect to GPS again - enum ReconnectDelay - { - ShortDelay = 3000, // 3 secs - LongDelay = 5000, // 5 secs - ExtraLongDelay = 10000 // 10 secs - }; - public: NetworkPositionProvider( const QString &addr, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); ~NetworkPositionProvider() override; diff --git a/app/position/providers/trimblepositionprovider.cpp b/app/position/providers/trimblepositionprovider.cpp new file mode 100644 index 000000000..adcabeb2f --- /dev/null +++ b/app/position/providers/trimblepositionprovider.cpp @@ -0,0 +1,430 @@ +/*************************************************************************** + * * + * 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 "trimblepositionprovider.h" + +#include +#include +#include +#include + +#include +#include +#include + +#ifdef ANDROID +#include +#include +#endif + +#include "coreutils.h" +// trimblesecrets.cpp is generated by decrypting trimblesecrets.cpp.enc. +// It defines static QString __getTrimbleAppId(). +#include "trimblesecrets.cpp" + +constexpr int ONE_SECOND_MS = 1000; +constexpr int TMM_WS_V2_DEFAULT_PORT = 9639; +// Trimble reports just 2D CRS however we have here the 3D variant for further elevation processing +const QHash TRIMBLE_REFERENCE_FRAMES = +{ + { QStringLiteral( "BGS2005" ), QStringLiteral( "EPSG:7797" ) }, + { QStringLiteral( "CGCS2000" ), QStringLiteral( "EPSG:4480" ) }, + { QStringLiteral( "CR-SIRGAS" ), QStringLiteral( "EPSG:8906" ) }, + { QStringLiteral( "EST97" ), QStringLiteral( "EPSG:4935" ) }, + { QStringLiteral( "ETRF1989" ), QStringLiteral( "EPSG:7914" ) }, + { QStringLiteral( "ETRF2000" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ETRF2000 (EPOCH:2010.5)" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ETRS89-D96-17" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "ETRS89-DREF91(R16)" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "EUREF-DK15" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "EUREF-DK94" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "EUREF-FIN" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "EUREF-NKG-2003" ), QStringLiteral( "EPSG:4951" ) }, + { QStringLiteral( "EUREF89" ), QStringLiteral( "EPSG:4937" ) }, + { QStringLiteral( "GDA2020" ), QStringLiteral( "EPSG:7843" ) }, + { QStringLiteral( "GDA94" ), QStringLiteral( "EPSG:4939" ) }, + { QStringLiteral( "ISN2016" ), QStringLiteral( "EPSG:8085" ) }, + { QStringLiteral( "ITRF1993" ), QStringLiteral( "EPSG:7905" ) }, + { QStringLiteral( "ITRF2000" ), QStringLiteral( "EPSG:7909" ) }, + { QStringLiteral( "ITRF2005" ), QStringLiteral( "EPSG:7910" ) }, + { QStringLiteral( "ITRF2008" ), QStringLiteral( "EPSG:7911" ) }, + { QStringLiteral( "ITRF2008-India-CORS" ), QStringLiteral( "EPSG:8999" ) }, + { QStringLiteral( "ITRF2008-Mexico" ), QStringLiteral( "EPSG:6364" ) }, + { QStringLiteral( "ITRF2014" ), QStringLiteral( "EPSG:7912" ) }, + { QStringLiteral( "ITRF2020" ), QStringLiteral( "EPSG:9989" ) }, + { QStringLiteral( "JGD2000" ), QStringLiteral( "EPSG:4947" ) }, + { QStringLiteral( "JGD2011" ), QStringLiteral( "EPSG:6667" ) }, + { QStringLiteral( "KGD2002" ), QStringLiteral( "EPSG:4927" ) }, + { QStringLiteral( "KSA-GRF17" ), QStringLiteral( "EPSG:9332" ) }, + { QStringLiteral( "LKS-92" ), QStringLiteral( "EPSG:4949" ) }, + { QStringLiteral( "MAGNA-SIRGAS" ), QStringLiteral( "EPSG:4997" ) }, + { QStringLiteral( "MAGNA-SIRGAS(2018)" ), QStringLiteral( "EPSG:4997" ) }, + { QStringLiteral( "MTRF-2000" ), QStringLiteral( "EPSG:8817" ) }, + { QStringLiteral( "NAD83(2011) (EPOCH:2010)" ), QStringLiteral( "EPSG:6319" ) }, + { QStringLiteral( "NAD83(2011) (EPOCH:2017.5)" ), QStringLiteral( "EPSG:6319" ) }, + { QStringLiteral( "NAD83(CORS96) (EPOCH:2002)" ), QStringLiteral( "EPSG:6782" ) }, + { QStringLiteral( "NAD83(CSRS)v7 (EPOCH:1997)" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "NAD83(CSRS)v7 (EPOCH:2002)" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "NAD83(CSRS)v7 (EPOCH:2010)" ), QStringLiteral( "EPSG:8254" ) }, + { QStringLiteral( "NAD83(CSRS)v8 (EPOCH:1997)" ), QStringLiteral( "EPSG:10413" ) }, + { QStringLiteral( "NAD83(CSRS)v8 (EPOCH:2002)" ), QStringLiteral( "EPSG:10413" ) }, + { QStringLiteral( "NAD83(CSRS)v8 (EPOCH:2010)" ), QStringLiteral( "EPSG:10413" ) }, + { QStringLiteral( "NAD83(MA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6324" ) }, + { QStringLiteral( "NAD83(PA11) (EPOCH:2010)" ), QStringLiteral( "EPSG:6321" ) }, + { QStringLiteral( "NZGD2000" ), QStringLiteral( "EPSG:4959" ) }, + { QStringLiteral( "OSNetv2009" ), QStringLiteral( "EPSG:4277" ) }, // TODO: create 3D CRS here + { QStringLiteral( "POSGAR07" ), QStringLiteral( "EPSG:5342" ) }, + { QStringLiteral( "PZ-90.11" ), QStringLiteral( "EPSG:7680" ) }, + { QStringLiteral( "RDN2008" ), QStringLiteral( "EPSG:6705" ) }, + { QStringLiteral( "RGAF09" ), QStringLiteral( "EPSG:5488" ) }, + { QStringLiteral( "RGF93v2b" ), QStringLiteral( "EPSG:9781" ) }, + { QStringLiteral( "RGFG95" ), QStringLiteral( "EPSG:4967" ) }, + { QStringLiteral( "RGNC91-93" ), QStringLiteral( "EPSG:4907" ) }, + { QStringLiteral( "RGR92" ), QStringLiteral( "EPSG:4971" ) }, + { QStringLiteral( "RGTAAF07" ), QStringLiteral( "EPSG:7072" ) }, + { QStringLiteral( "SIRGAS-Chile 2016" ), QStringLiteral( "EPSG:9152" ) }, + { QStringLiteral( "SIRGAS-Chile 2021" ), QStringLiteral( "EPSG:20040" ) }, + { QStringLiteral( "SIRGAS-CON" ), QStringLiteral( "EPSG:4989" ) }, + { QStringLiteral( "SIRGAS-CON SIR17P01" ), QStringLiteral( "EPSG:8946" ) }, + { QStringLiteral( "SIRGAS-ROU98" ), QStringLiteral( "EPSG:5380" ) }, + { QStringLiteral( "SIRGAS2000" ), QStringLiteral( "EPSG:4989" ) }, + { QStringLiteral( "SWEREF99" ), QStringLiteral( "EPSG:4977" ) }, + { QStringLiteral( "WGS84 (current)" ), QStringLiteral( "EPSG:4979" ) }, +}; + +TrimblePositionProvider::TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent ) + : AbstractPositionProvider( id, QStringLiteral( "external_trimble" ), name, positionTransformer, parent ) + , mSecondsLeftToReconnect( ReconnectDelay::ShortDelay / ONE_SECOND_MS ) +{ + mRegistration = new TrimbleRegistration( this ); + connect( mRegistration, &TrimbleRegistration::registered, this, &TrimblePositionProvider::onRegistered ); + connect( mRegistration, &TrimbleRegistration::failed, this, &TrimblePositionProvider::onRegistrationFailed ); + + mReconnectTimer.setSingleShot( false ); + mReconnectTimer.setInterval( ONE_SECOND_MS ); + connect( &mReconnectTimer, &QTimer::timeout, this, &TrimblePositionProvider::onReconnectTimeout ); + + mHeartBeatTimer.setSingleShot( true ); + connect( &mHeartBeatTimer, &QTimer::timeout, this, [this] + { + setState( tr( "No data" ), State::NoConnection ); + emit positionChanged( GeoPosition() ); + startReconnectTimer(); + } ); + + TrimblePositionProvider::startUpdates(); +} + +TrimblePositionProvider::~TrimblePositionProvider() +{ + TrimblePositionProvider::closeProvider(); +} + +void TrimblePositionProvider::startUpdates() +{ + if ( mRegistrationInProgress ) + { + return; + } + + if ( mCachedPort > 0 ) + { + connectWebSocket( mCachedPort ); + return; + } + + mRegistrationInProgress = true; + setState( tr( "Connecting" ), State::Connecting ); + mRegistration->requestRegistration( __getTrimbleAppId() ); +} + + +void TrimblePositionProvider::stopUpdates() +{ + mHeartBeatTimer.stop(); + mReconnectTimer.stop(); + if ( mSocket && mSocket->state() == QAbstractSocket::ConnectedState ) + mSocket->close(); +} + +void TrimblePositionProvider::closeProvider() +{ + mHeartBeatTimer.stop(); + mReconnectTimer.stop(); + if ( mSocket ) + { + mSocket->disconnect(); + mSocket->abort(); + } +} + +QgsCoordinateReferenceSystem TrimblePositionProvider::resolveFrame( const QString &frameName, const double epoch ) +{ + // fallback to PositionKit::positionCrsXXX() + if ( frameName.isEmpty() ) return {}; + + const QString crsId = TRIMBLE_REFERENCE_FRAMES.value( frameName ); + if ( !crsId.isEmpty() ) + { + QgsCoordinateReferenceSystem newCrs( crsId ); + if ( epoch >= 0 ) + { + newCrs.setCoordinateEpoch( epoch ); + } + return newCrs; + } + + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "TrimblePositionProvider: unknown frame '%1', falling back to WGS84" ).arg( frameName ) ); + return {}; +} + +GeoPosition TrimblePositionProvider::parseLocationMessage( const QString &json ) +{ + GeoPosition pos; + + const QJsonDocument doc = QJsonDocument::fromJson( json.toUtf8() ); + if ( doc.isNull() || !doc.isObject() ) + return pos; + + const QJsonObject obj = doc.object(); + + if ( obj.contains( QStringLiteral( "latitude" ) ) && !obj.value( QStringLiteral( "latitude" ) ).isNull() ) + pos.latitude = obj.value( QStringLiteral( "latitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + + if ( obj.contains( QStringLiteral( "longitude" ) ) && !obj.value( QStringLiteral( "longitude" ) ).isNull() ) + pos.longitude = obj.value( QStringLiteral( "longitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + + if ( obj.contains( QStringLiteral( "undulation" ) ) && !obj.value( QStringLiteral( "undulation" ) ).isNull() ) + { + pos.elevation_diff = obj.value( QStringLiteral( "undulation" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + // prefer msl height if available + if ( obj.contains( QStringLiteral( "mslHeight" ) ) && !obj.value( QStringLiteral( "mslHeight" ) ).isNull() ) + { + pos.elevation = obj.value( QStringLiteral( "mslHeight" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + } + } + else if ( obj.contains( QStringLiteral( "altitude" ) ) && !obj.value( QStringLiteral( "altitude" ) ).isNull() ) + { + pos.elevation = obj.value( QStringLiteral( "altitude" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + } + + if ( obj.contains( QStringLiteral( "speed" ) ) && !obj.value( QStringLiteral( "speed" ) ).isNull() ) + { + pos.speed = obj.value( QStringLiteral( "speed" ) ).toDouble( -1 ); + // trimble reports speed in m/s, and speed in QgsGpsInformation is in km/h + if ( pos.speed >= 0 ) pos.speed = pos.speed * 3.6; + } + + if ( obj.contains( QStringLiteral( "bearing" ) ) && !obj.value( QStringLiteral( "bearing" ) ).isNull() ) + pos.direction = obj.value( QStringLiteral( "bearing" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "pdop" ) ) && !obj.value( QStringLiteral( "pdop" ) ).isNull() ) + pos.pdop = obj.value( QStringLiteral( "pdop" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "hdop" ) ) && !obj.value( QStringLiteral( "hdop" ) ).isNull() ) + pos.hdop = obj.value( QStringLiteral( "hdop" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "vdop" ) ) && !obj.value( QStringLiteral( "vdop" ) ).isNull() ) + pos.vdop = obj.value( QStringLiteral( "vdop" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "hrms" ) ) && !obj.value( QStringLiteral( "hrms" ) ).isNull() ) + pos.hacc = obj.value( QStringLiteral( "hrms" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "vrms" ) ) && !obj.value( QStringLiteral( "vrms" ) ).isNull() ) + pos.vacc = obj.value( QStringLiteral( "vrms" ) ).toDouble( -1 ); + + if ( obj.contains( QStringLiteral( "totalSatInUse" ) ) && !obj.value( QStringLiteral( "totalSatInUse" ) ).isNull() ) + pos.satellitesUsed = obj.value( QStringLiteral( "totalSatInUse" ) ).toInt( -1 ); + + if ( obj.contains( QStringLiteral( "satellites" ) ) && !obj.value( QStringLiteral( "satellites" ) ).isNull() ) + pos.satellitesVisible = obj.value( QStringLiteral( "satellites" ) ).toInt( -1 ); + + if ( obj.contains( QStringLiteral( "utcTimeStamp" ) ) && !obj.value( QStringLiteral( "utcTimeStamp" ) ).isNull() ) + pos.utcDateTime = obj.value( QStringLiteral( "utcTimeStamp" ) ).toVariant().toDateTime(); + + const int diffStatus = obj.value( QStringLiteral( "diffStatus" ) ).toInt( -1 ); + switch ( diffStatus ) + { + case 1: + pos.qualityIndicator = Qgis::GpsQualityIndicator::GPS; + break; + case 2: + pos.qualityIndicator = Qgis::GpsQualityIndicator::DGPS; + break; + case 4: + pos.qualityIndicator = Qgis::GpsQualityIndicator::RTK; + break; + case 5: + pos.qualityIndicator = Qgis::GpsQualityIndicator::FloatRTK; + break; + default: + pos.qualityIndicator = Qgis::GpsQualityIndicator::Unknown; + break; + } + pos.fixStatusString = pos.parseFixStatus(); + + if ( obj.contains( QStringLiteral( "antennaHeight" ) ) && !obj.value( QStringLiteral( "antennaHeight" ) ).isNull() ) + { + mAntennaHeight = obj.value( QStringLiteral( "antennaHeight" ) ).toDouble( std::numeric_limits::quiet_NaN() ); + } + + const bool referenceFrameFieldExists = obj.contains( QStringLiteral( "targetReferenceFrameName" ) ) && !obj.value( QStringLiteral( "targetReferenceFrameName" ) ).isNull() ; + const bool referenceFrameEpochFieldExists = obj.contains( QStringLiteral( "targetReferenceFrameEpoch" ) ) && !obj.value( QStringLiteral( "targetReferenceFrameEpoch" ) ).isNull(); + + if ( referenceFrameFieldExists && referenceFrameEpochFieldExists ) + { + const QString frameName = obj.value( QStringLiteral( "targetReferenceFrameName" ) ).toString(); + const double epoch = obj.value( QStringLiteral( "targetReferenceFrameEpoch" ) ).toDouble( -1 ); + const QgsCoordinateReferenceSystem newCrs = resolveFrame( frameName, epoch ); + + if ( newCrs != mSourceCrs ) + { + mSourceCrs = newCrs; + mPositionTransformer->setSourceCrs( newCrs ); + } + } + + if ( obj.contains( QStringLiteral( "geoidModel" ) ) && !obj.value( QStringLiteral( "geoidModel" ) ).isNull() ) + { + mGeoidModelName = obj.value( QStringLiteral( "geoidModel" ) ).toString(); + } + else + { + mGeoidModelName = QString(); + } + + return pos; +} + +QgsCoordinateReferenceSystem TrimblePositionProvider::sourceCrs() const +{ + return mSourceCrs; +} + +QString TrimblePositionProvider::geoidModelName() const +{ + return mGeoidModelName; +} + +double TrimblePositionProvider::antennaHeight() const +{ + return mAntennaHeight; +} + +void TrimblePositionProvider::onRegistered( const int port ) +{ + mRegistrationInProgress = false; + mCachedPort = port; + connectWebSocket( port ); +} + +void TrimblePositionProvider::onRegistrationFailed( const QString &reason ) +{ + mRegistrationInProgress = false; + setState( reason, State::NoConnection ); + emit positionChanged( GeoPosition() ); +} + +void TrimblePositionProvider::connectWebSocket( const int port ) +{ + mSocket = std::make_unique(); + + connect( mSocket.get(), &QWebSocket::textMessageReceived, this, &TrimblePositionProvider::onTextMessageReceived ); + connect( mSocket.get(), &QWebSocket::disconnected, this, &TrimblePositionProvider::onSocketDisconnected ); + connect( mSocket.get(), &QWebSocket::errorOccurred, this, &TrimblePositionProvider::onSocketError ); + connect( mSocket.get(), &QWebSocket::connected, this, [this] + { + setState( tr( "Connected" ), State::Connected ); + mReconnectDelay = ReconnectDelay::ShortDelay; + mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); + } ); + + const QUrl url( QStringLiteral( "ws://localhost:%2" ).arg( port ) ); + setState( tr( "Connecting" ), State::Connecting ); + mSocket->open( url ); +} + +void TrimblePositionProvider::onTextMessageReceived( const QString &message ) +{ + mHeartBeatTimer.start( ReconnectDelay::ExtraLongDelay ); + + GeoPosition parsedPosition = parseLocationMessage( message ); + GeoPosition newPosition = mPositionTransformer->processTrimblePosition( parsedPosition ); + + setState( tr( "Connected" ), State::Connected ); + emit positionChanged( newPosition ); +} + +void TrimblePositionProvider::onSocketError( const QAbstractSocket::SocketError error ) +{ + Q_UNUSED( error ) + setState( tr( "Disconnected" ), State::NoConnection ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Socket error occurred: %1" ).arg( mSocket->errorString() ) ); + emit positionChanged( GeoPosition() ); + startReconnectTimer(); +} + +void TrimblePositionProvider::onSocketDisconnected() +{ + mHeartBeatTimer.stop(); + setState( tr( "Disconnected" ), State::NoConnection ); + emit positionChanged( GeoPosition() ); + startReconnectTimer(); +} + +void TrimblePositionProvider::startReconnectTimer() +{ + mSecondsLeftToReconnect = mReconnectDelay / ONE_SECOND_MS; + setState( tr( "Reconnecting in %1 s" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + mReconnectTimer.start(); + + if ( mReconnectDelay == ReconnectDelay::ShortDelay ) + mReconnectDelay = ReconnectDelay::LongDelay; +} + +void TrimblePositionProvider::onReconnectTimeout() +{ + if ( mSecondsLeftToReconnect <= 1 ) + { + reconnect(); + } + else + { + mSecondsLeftToReconnect--; + setState( tr( "Reconnecting in %1 s" ).arg( mSecondsLeftToReconnect ), State::WaitingToReconnect ); + } +} + +void TrimblePositionProvider::reconnect() +{ + mReconnectTimer.stop(); + if ( mCachedPort > 0 ) + { + connectWebSocket( mCachedPort ); + } + else + { + startUpdates(); // re-register + } +} + +void TrimblePositionProvider::openAntennaHeightPage() +{ +#ifdef ANDROID + QJniObject intentAction = QJniObject::fromString( QStringLiteral( "com.trimble.tmm.OPENANTENNAHEIGHT" ) ); + QJniObject intent( "android/content/Intent", "(Ljava/lang/String;)V", intentAction.object() ); + QJniObject activity = QJniObject::callStaticObjectMethod( "org/qtproject/qt/android/QtNative", + "activity", + "()Landroid/app/Activity;" ); + if ( activity.isValid() ) + activity.callMethod( "startActivity", "(Landroid/content/Intent;)V", intent.object() ); +#elif defined(Q_OS_IOS) + QDesktopServices::openUrl( QUrl( QStringLiteral( "TmmOpenToAntennaHeight://trimble.tmm.iOS" ) ) ); +#endif +} diff --git a/app/position/providers/trimblepositionprovider.h b/app/position/providers/trimblepositionprovider.h new file mode 100644 index 000000000..3ca93d86e --- /dev/null +++ b/app/position/providers/trimblepositionprovider.h @@ -0,0 +1,71 @@ +/*************************************************************************** + * * + * 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 TRIMBLEPOSITIONPROVIDER_H +#define TRIMBLEPOSITIONPROVIDER_H + +#include +#include + +#include "abstractpositionprovider.h" +#include "trimbleregistration.h" + +class TrimblePositionProvider : public AbstractPositionProvider +{ + Q_OBJECT + + public: + TrimblePositionProvider( const QString &id, const QString &name, PositionTransformer &positionTransformer, QObject *parent = nullptr ); + ~TrimblePositionProvider() override; + + void startUpdates() override; + void stopUpdates() override; + void closeProvider() override; + + QgsCoordinateReferenceSystem sourceCrs() const; + QString geoidModelName() const; + + // trimble provider provides antenna height from Trimble Mobile Manager + double antennaHeight() const; + // TODO: move to native utils + Q_INVOKABLE void openAntennaHeightPage(); + + private slots: + void onRegistered( int port ); + void onRegistrationFailed( const QString &reason ); + void onTextMessageReceived( const QString &message ); + void onSocketError( QAbstractSocket::SocketError error ); + void onSocketDisconnected(); + void onReconnectTimeout(); + + private: + void connectWebSocket( int port ); + void startReconnectTimer(); + void reconnect(); + + GeoPosition parseLocationMessage( const QString &json ); + static QgsCoordinateReferenceSystem resolveFrame( const QString &frameName, double epoch ); + + TrimbleRegistration *mRegistration = nullptr; + std::unique_ptr mSocket; + + QTimer mReconnectTimer; + QTimer mHeartBeatTimer; + int mReconnectDelay = ReconnectDelay::ShortDelay; + int mSecondsLeftToReconnect = 0; + + int mCachedPort = 0; + bool mRegistrationInProgress = false; + + QgsCoordinateReferenceSystem mSourceCrs; + double mAntennaHeight = std::numeric_limits::quiet_NaN(); + QString mGeoidModelName; +}; + +#endif // TRIMBLEPOSITIONPROVIDER_H diff --git a/app/position/providers/trimbleregistration.h b/app/position/providers/trimbleregistration.h new file mode 100644 index 000000000..64286826e --- /dev/null +++ b/app/position/providers/trimbleregistration.h @@ -0,0 +1,72 @@ +/*************************************************************************** + * * + * 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 TRIMBLEREGISTRATION_H +#define TRIMBLEREGISTRATION_H + +#include +#include +#include + +#ifdef Q_OS_ANDROID +#include +class TrimbleRegistration; +#endif + +#ifdef Q_OS_ANDROID + +class TrimbleResultReceiver : public QObject, public QAndroidActivityResultReceiver +{ + Q_OBJECT + + public: + void handleActivityResult( int receiverRequestCode, int resultCode, const QJniObject &data ) override; + + signals: + void registrationFailed( QString reason ); + void registrationSucceeded( int locationDataPort ); +}; +#endif + + +/** + * Platform-agnostic async contract for registering with Trimble Mobile Manager. + * + * Call requestRegistration() once; listen for registered() or failed(). + * Concrete implementations are in trimbleregistrationandroid.cpp (Android) + * and trimbleregistrationios.mm (iOS). + * + * TODO: move to native utils when refactoring native utils + */ +class TrimbleRegistration : public QObject +{ + Q_OBJECT + + public: + explicit TrimbleRegistration( QObject *parent = nullptr ); + ~TrimbleRegistration() override = default; + + void requestRegistration( const QString &appId ); + +#ifdef Q_OS_IOS + slots: + void handleCallback( const QUrl &url ); +#endif + + signals: + void registered( int locationV2Port ); + void failed( const QString &reason ); + + private: +#ifdef Q_OS_ANDROID + std::unique_ptr mResultReceiver; +#endif +}; + +#endif // TRIMBLEREGISTRATION_H diff --git a/app/position/providers/trimbleregistrationandroid.cpp b/app/position/providers/trimbleregistrationandroid.cpp new file mode 100644 index 000000000..8966457d3 --- /dev/null +++ b/app/position/providers/trimbleregistrationandroid.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + * * + * 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 "trimbleregistration.h" + +#ifdef Q_OS_ANDROID + +#include + +#include "coreutils.h" + +static constexpr int TMM_REGISTER_REQUEST_CODE = 0x544D4D52; // "TMMR" - custom value to pair intent result + +void TrimbleResultReceiver::handleActivityResult( const int receiverRequestCode, const int resultCode, const QJniObject &data ) +{ + Q_UNUSED( resultCode ) + if ( receiverRequestCode != TMM_REGISTER_REQUEST_CODE ) + return; + + if ( !data.isValid() ) + { + emit registrationFailed( TrimbleRegistration::tr( "No response from Trimble Mobile Manager" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); + return; + } + + const QAndroidIntent intent( data ); + // we can't use intent.extraVariant() function here as it throws errors in Qt code + const QString registerResult = intent.handle().callObjectMethod( "getStringExtra", QJniObject::fromString( QStringLiteral( "registrationResult" ) ).object() ).toString(); + + if ( registerResult != QStringLiteral( "OK" ) ) + { + emit registrationFailed( TrimbleRegistration::tr( "Trimble Mobile Manager registration failed: %1" ).arg( registerResult ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, registration has been refused by Trimble Mobile Manager. Reason: %1." ).arg( registerResult ) ); + return; + } + // we can't use intent.extraVariant() function here as it doesn't parse the data correctly + const int locationPortResult = intent.handle().callMethod( "getIntExtra", QJniObject::fromString( QStringLiteral( "locationV2Port" ) ).object(), 0 ); + + if ( !locationPortResult ) + { + emit registrationFailed( TrimbleRegistration::tr( "Trimble Mobile Manager returned invalid port" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager returned malformed location data port." ) ); + return; + } + + emit registrationSucceeded( locationPortResult ); +} + +TrimbleRegistration::TrimbleRegistration( QObject *parent ) + : QObject( parent ) + , mResultReceiver( std::make_unique() ) +{ + connect( mResultReceiver.get(), &TrimbleResultReceiver::registrationFailed, this, &TrimbleRegistration::failed ); + connect( mResultReceiver.get(), &TrimbleResultReceiver::registrationSucceeded, this, &TrimbleRegistration::registered ); +} + +void TrimbleRegistration::requestRegistration( const QString &appId ) +{ + const QAndroidIntent intent( QStringLiteral( "com.trimble.tmm.REGISTER" ) ); + // we can't use intent.extraVariant() function here as it writes the string as byte array instead of strings + intent.handle().callObjectMethod( "putExtra", QJniObject::fromString( QStringLiteral( "applicationID" ) ).object(), QJniObject::fromString( appId ).object() ); + + QtAndroidPrivate::startActivity( intent, TMM_REGISTER_REQUEST_CODE, mResultReceiver.get() ); +} + +#endif // ANDROID diff --git a/app/position/providers/trimbleregistrationios.mm b/app/position/providers/trimbleregistrationios.mm new file mode 100644 index 000000000..1455203a1 --- /dev/null +++ b/app/position/providers/trimbleregistrationios.mm @@ -0,0 +1,117 @@ +/*************************************************************************** + * * + * 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 "trimbleregistration.h" + +#ifdef Q_OS_IOS + +#include +#include +#include +#include +#include +#include +#include + +#import + +static QString MM_CALLBACK_SCHEME = QStringLiteral( "merginmaps" ); +static QString MM_CALLBACK_URL = QStringLiteral( "tmm-registration" ); + +TrimbleRegistration::TrimbleRegistration( QObject *parent ) + : QObject( parent ) +{ +} + +void TrimbleRegistration::requestRegistration( const QString &appId ) +{ + QDesktopServices::setUrlHandler( MM_CALLBACK_SCHEME, this, "handleCallback" ); + + QJsonObject payload; + payload[QStringLiteral( "application_id" )] = appId; + payload[QStringLiteral( "returl" )] = QStringLiteral( "%1://%2" ).arg( MM_CALLBACK_SCHEME, MM_CALLBACK_URL ); + + const QByteArray jsonBytes = QJsonDocument( payload ).toJson( QJsonDocument::Compact ); + const QString base64 = QString::fromLatin1( jsonBytes.toBase64() ); + + // QDesktopServices::openUrl() routes through QUrl which percent-encodes or rejects + // the base64 payload, breaking the scheme. + NSString *nsUrlString = QStringLiteral( "%1://?%2" ).arg( QStringLiteral( "tmmregister" ), base64 ).toNSString(); + NSURL *trimbleUrl = [NSURL URLWithString:nsUrlString]; + + if ( !trimbleUrl ) + { + emit failed( tr( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, trimble URL is malformed." ) ); + return; + } + + QPointer self( this ); + [[UIApplication sharedApplication] openURL:trimbleUrl options:@ {} completionHandler: ^ ( BOOL success ) + { + if ( !success && self ) + { + emit self->failed( tr( "Registration failed, no response from Trimble Mobile Manager, probably it's missing." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, failed to open Trimble Mobile Manager, probably it's missing." ) ); + } + }]; +} + +void TrimbleRegistration::handleCallback( const QUrl &url ) +{ + if ( url.scheme() != MM_CALLBACK_SCHEME ) + { + emit self->failed( tr( "Registration failed, wrong response from Trimble Mobile Manager." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager responded with wrong scheme." ) ); + return; + } + + if ( url.host() != MM_CALLBACK_URL ) + { + emit self->failed( tr( "Registration failed, wrong response from Trimble Mobile Manager." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager responded to wrong url." ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); + return; + } + + const QByteArray jsonBytes = QByteArray::fromBase64( url.query().toLatin1() ); + const QJsonDocument doc = QJsonDocument::fromJson( jsonBytes ); + if ( doc.isNull() || !doc.isObject() ) + { + emit failed( tr( "Registration failed, wrong response from Trimble Mobile Manager." ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager responded with malformed JSON." ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); + return; + } + + const QJsonObject obj = doc.object(); + const QString registerResult = obj.value( QStringLiteral( "registrationResult" ) ).toString(); + + if ( registerResult != QLatin1String( "OK" ) ) + { + emit failed( tr( "Trimble Mobile Manager registration failed: %1" ).arg( registerResult ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, registration has been refused by Trimble Mobile Manager. Reason: %1." ).arg( registerResult ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); + return; + } + + const int locationPort = obj.value( QStringLiteral( "locationV2Port" ) ).toInt( -1 ); + if ( locationPort <= 0 ) + { + emit failed( tr( "Trimble Mobile Manager returned invalid port" ) ); + CoreUtils::log( QStringLiteral( "TrimblePositionProvider" ), QStringLiteral( "Registration failed, Trimble Mobile Manager returned malformed location data port." ) ); + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); + return; + } + + QDesktopServices::unsetUrlHandler( QString::fromLatin1( MM_CALLBACK_SCHEME ) ); + emit registered( locationPort ); +} + +#endif // Q_OS_IOS diff --git a/app/position/providers/trimblesecrets.cpp.enc b/app/position/providers/trimblesecrets.cpp.enc new file mode 100644 index 000000000..868dae3a7 Binary files /dev/null and b/app/position/providers/trimblesecrets.cpp.enc differ diff --git a/app/qml/gps/MMExternalProviderConnectionDrawer.qml b/app/qml/gps/MMExternalProviderConnectionDrawer.qml index bdcdb3d6f..93153868a 100644 --- a/app/qml/gps/MMExternalProviderConnectionDrawer.qml +++ b/app/qml/gps/MMExternalProviderConnectionDrawer.qml @@ -30,20 +30,12 @@ MMComponents.MMDrawer { states: [ State { - name: "working" + name: "connecting" when: root.positionProvider && root.positionProvider.state === PositionProvider.Connecting PropertyChanges { - message.image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage - message.title: root.providerType === "network" - ? qsTr( "Connecting to external receiver" ) - : ( root.positionProvider.name() - ? qsTr( "Connecting to" ) + " " + root.positionProvider.name() - : qsTr( "Connecting" ) + root.connectingSuffixAnimation ) - message.description: root.providerType === "bluetooth" - ? qsTr( "You might be asked to pair your device during this process." ) - : ( root.positionProvider.id() - ? qsTr( "Connecting to" ) + " " + root.positionProvider.id() + qsTr( ". You can close this panel, the app will continue in the background." ) - : qsTr( "Connecting" ) + root.connectingSuffixAnimation ) + message.image: root.getExternalProviderImage() + message.title: root.getConnectingTitle() + message.description: root.getConnectingDescription() message.linkText: "" } }, @@ -51,7 +43,7 @@ MMComponents.MMDrawer { name: "success" when: root.positionProvider && root.positionProvider.state === PositionProvider.Connected PropertyChanges { - message.image: root.providerType === "bluetooth" ? __style.externalBluetoothGreenImage : __style.externalNetworkGreenImage + message.image: root.getExternalProviderImage() message.title: qsTr( "Connected" ) message.description: "" message.linkText: "" @@ -62,12 +54,8 @@ MMComponents.MMDrawer { when: !root.positionProvider || root.positionProvider.state === PositionProvider.NoConnection PropertyChanges { message.image: __style.externalGpsRedImage - message.title: qsTr( "Failed to connect to" ) + " " + ( root.positionProvider - ? ( root.providerType === "network" ? root.positionProvider.id() : root.positionProvider.name() ) - : "" ) - message.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." ) + message.title: root.getFailTitle() + message.description: root.getFailDescription() message.linkText: qsTr( "Learn more" ) } }, @@ -76,16 +64,14 @@ MMComponents.MMDrawer { when: root.positionProvider && root.positionProvider.state === PositionProvider.WaitingToReconnect PropertyChanges { message.image: __style.externalGpsRedImage - message.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." ) - message.description: root.positionProvider.stateMessage + "

" + qsTr( "You can close this message, we will try to repeatedly connect to your device." ) + message.title: root.getWaitingToReconnectTitle() + message.description: qsTr( "%1%2You can close this message, we will try to repeatedly connect to your device." ).arg( root.positionProvider.stateMessage ).arg( "

" ) message.linkText: qsTr( "Learn more" ) } } ] - state: "working" + state: "connecting" } drawerBottomMargin: __style.margin40 @@ -116,7 +102,7 @@ MMComponents.MMDrawer { interval: 400 repeat: true - running: rootstate.state === "working" + running: rootstate.state === "connecting" onTriggered: { if ( root.connectingSuffixAnimation.length > 2 ) { @@ -127,4 +113,78 @@ MMComponents.MMDrawer { } } } + + function getExternalProviderImage() { + switch ( root.providerType ) { + case "bluetooth": + return __style.externalBluetoothGreenImage + case "network": + return __style.externalNetworkGreenImage + case "trimble": + return __style.externalNetworkGreenImage + default: + return "" + } + } + + function getConnectingTitle() { + if ( root.providerType === "network" ) { + return qsTr( "Connecting to external receiver" ) + } + else { + if ( root.positionProvider.name() ) { + return qsTr( "Connecting to %1" ).arg( root.positionProvider.name() ) + } + else { + return qsTr( "Connecting%1" ).arg( root.connectingSuffixAnimation ) + } + } + } + + function getConnectingDescription() { + if ( root.providerType === "bluetooth" ) { + return qsTr( "You might be asked to pair your device during this process." ) + } + else { + if ( root.positionProvider.id() && root.providerType !== "trimble" ) { + return qsTr( "Connecting to %1. You can close this panel, the app will continue in the background." ).arg( root.positionProvider.id() ) + } + else { + return qsTr( "Connecting%1" ).arg( root.connectingSuffixAnimation ) + } + } + } + + function getFailTitle() { + if ( root.providerType === "trimble" || !root.positionProvider ) { + return qsTr( "Failed to connect" ) + } + else { + let providerName + if ( root.providerType === "network" ) { + providerName = root.positionProvider.id() + } + else { + providerName = root.positionProvider.name() + } + + return qsTr( "Failed to connect to %1" ).arg( providerName ) + } + } + + function getFailDescription() { + 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." ) + } + else if ( root.providerType === "trimble" ) { + return qsTr( "We were not able to connect to Trimble Mobile Manager. Please make sure it's installed." ) + } + } + + function getWaitingToReconnectTitle() { + return getFailDescription() + } } diff --git a/app/qml/gps/MMGpsDataDrawer.qml b/app/qml/gps/MMGpsDataDrawer.qml index b375b29cd..b13a455d3 100644 --- a/app/qml/gps/MMGpsDataDrawer.qml +++ b/app/qml/gps/MMGpsDataDrawer.qml @@ -318,7 +318,7 @@ MMComponents.MMDrawer { width: parent.width / 2 title: qsTr( "GPS antenna height" ) - value: AppSettings.gpsAntennaHeight > 0 ? __inputUtils.formatNumber(AppSettings.gpsAntennaHeight, 3) + " m" : qsTr( "Not set" ) + value: PositionKit.antennaHeight > 0 ? __inputUtils.formatNumber(PositionKit.antennaHeight, 3) + " m" : qsTr( "Not set" ) alignmentRight: Positioner.index % 2 === 1 } diff --git a/app/qml/gps/MMPositionProviderPage.qml b/app/qml/gps/MMPositionProviderPage.qml index 34ceb63ff..52732a707 100644 --- a/app/qml/gps/MMPositionProviderPage.qml +++ b/app/qml/gps/MMPositionProviderPage.qml @@ -133,7 +133,7 @@ MMComponents.MMPage { text: qsTr( "Connect new receiver" ) onClicked: { - if ( __haveBluetooth ) { + if ( PositionKit.hasBluetoothSupport || PositionKit.hasTrimbleSupport ) { providerTypeDrawer.open() } else { @@ -148,6 +148,9 @@ MMComponents.MMPage { onProviderSelected: function( providerType ) { if ( providerType === "bluetooth" ) bluetoothDiscoveryLoader.active = true else if ( providerType === "network" ) networkProviderDrawer.open() + else if ( providerType === "trimble" ) { + root.activateProvider( "external_trimble", "trimble_tmm", qsTr( "Trimble Mobile Manager" ) ) + } } } @@ -235,7 +238,7 @@ MMComponents.MMPage { asynchronous: true sourceComponent: Component { MMExternalProviderConnectionDrawer{} } - onLoaded: { + onLoaded: () => { item.providerType = connectingDialogLoader.providerType item.open() } @@ -283,5 +286,8 @@ MMComponents.MMPage { else if ( type === "external_ip" ) { connectingDialogLoader.open( "network" ) } + else if ( type === "external_trimble" ) { + connectingDialogLoader.open( "trimble" ) + } } } diff --git a/app/qml/gps/MMProviderTypeDrawer.qml b/app/qml/gps/MMProviderTypeDrawer.qml index 745b0db56..d4464ab7a 100644 --- a/app/qml/gps/MMProviderTypeDrawer.qml +++ b/app/qml/gps/MMProviderTypeDrawer.qml @@ -21,16 +21,25 @@ MMComponents.MMListDrawer { drawerHeader.title: qsTr( "Connect new receiver" ) drawerHeader.titleFont: __style.t2 - onOpened: root.list.currentIndex = -1 + onOpened: list.currentIndex = -1 + + Component.onCompleted: () => { + root.list.currentIndex = -1 + } list.model: ListModel { id: providerTypeModel - Component.onCompleted: { - providerTypeModel.append( [ - { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon }, - { name: qsTr( "Network (TCP, UDP)" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } - ] ) + Component.onCompleted: () => { + if ( PositionKit.hasBluetoothSupport ) { + providerTypeModel.append( { name: qsTr( "Bluetooth" ), description: qsTr( "Bad Elf, Emlid, Juniper, marXact and more" ), type: "bluetooth", icon: __style.bluetoothIcon } ) + } + + providerTypeModel.append( { name: qsTr( "Network (TCP, UDP)" ), description: qsTr( "Emlid RS, EOS and more" ), type: "network", icon: __style.networkIcon } ) + + if ( PositionKit.hasTrimbleSupport ) { + providerTypeModel.append( { name: qsTr( "Trimble" ), description: qsTr( "Trimble receivers via Trimble Mobile Manager" ), type: "trimble", icon: __style.gpsIcon } ) + } } } @@ -77,7 +86,7 @@ MMComponents.MMListDrawer { description: parent.description checked: parent.ListView.isCurrentItem - onClicked: { + onClicked: () => { if ( parent.ListView.isCurrentItem ) { parent.ListView.view.currentIndex = -1 } @@ -94,16 +103,11 @@ MMComponents.MMListDrawer { text: qsTr( "Continue" ) enabled: root.list.currentIndex !== -1 - onClicked: { + onClicked: () => { const providerType = providerTypeModel.get( root.list.currentIndex ).type root.close() - if ( providerType === "bluetooth" ) { - root.providerSelected("bluetooth") - } - else if ( providerType === "network" ) { - root.providerSelected("network") - } + root.providerSelected( providerType ) } } } diff --git a/app/qml/settings/MMSettingsPage.qml b/app/qml/settings/MMSettingsPage.qml index 26435fddc..761b824a6 100644 --- a/app/qml/settings/MMSettingsPage.qml +++ b/app/qml/settings/MMSettingsPage.qml @@ -85,16 +85,45 @@ MMPage { MMLine {} - MMSettingsComponents.MMSettingsInput { + Loader { width: parent.width - title: qsTr("GPS antenna height") - description: qsTr("Includes pole height and GPS receiver’s antenna height") - valueDescription: qsTr("GPS antenna height, in meters") - value: AppSettings.gpsAntennaHeight - suffix: " m" + sourceComponent: !PositionKit.requireAntennaHeightTransform() ? externalAntennaHeightComponent : editableAntennaHeightComponent + } - onValueWasChanged: function( newValue ) { - AppSettings.gpsAntennaHeight = newValue + Component { + id: editableAntennaHeightComponent + + MMSettingsComponents.MMSettingsInput { + width: parent ? parent.width : 0 + title: qsTr("GPS antenna height") + description: qsTr("Includes pole height and GPS receiver’s antenna height") + valueDescription: qsTr("GPS antenna height, in meters") + value: AppSettings.gpsAntennaHeight + suffix: " m" + + onValueWasChanged: function( newValue ) { + AppSettings.gpsAntennaHeight = newValue + } + } + } + + Component { + id: externalAntennaHeightComponent + + Column { + width: parent ? parent.width : 0 + spacing: __style.spacing4 + + MMSettingsComponents.MMSettingsItem { + width: parent.width + title: qsTr("GPS antenna height") + description: qsTr("Click here to modify in Trimble Mobile Manager") + value: Number.isNaN( PositionKit.antennaHeight ) ? qsTr( "N/A" ) : __inputUtils.formatNumber( PositionKit.antennaHeight, 3 ) + " m" + onClicked: () => { + if ( PositionKit.positionProvider ) + PositionKit.openAntennaHeightPage() + } + } } } diff --git a/app/test/testmaptools.cpp b/app/test/testmaptools.cpp index fc70b296d..6185cda04 100644 --- a/app/test/testmaptools.cpp +++ b/app/test/testmaptools.cpp @@ -13,6 +13,8 @@ #include #include +#include "mmconfig.h" + #include "qgspoint.h" #include "qgslinestring.h" #include "qgspolygon.h" @@ -35,7 +37,7 @@ #include "featurelayerpair.h" #include "streamingintervaltype.h" -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -3053,7 +3055,7 @@ void TestMapTools::testAntennaHeight() void TestMapTools::testSmallTracking() { -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS QgsVectorLayer *pointLayer = new QgsVectorLayer( QStringLiteral( "PointZ?crs=epsg:4326" ), QString(), QStringLiteral( "memory" ) ); RecordingMapTool mapTool; @@ -3078,7 +3080,7 @@ void TestMapTools::testSmallTracking() mapTool.setActiveLayer( pointLayer ); mapTool.setActiveFeature( QgsFeature() ); - BluetoothPositionProvider *btProvider = new BluetoothPositionProvider( "AA:AA:FF:AA:00:10", "testBluetoothProvider" ); + AbstractPositionProvider *btProvider = mPositionKit->constructProvider( QStringLiteral( "external_bt" ), QStringLiteral( "AA:AA:FF:AA:00:10" ), QStringLiteral( "testBluetoothProvider" ) ); mPositionKit->setPositionProvider( btProvider ); NmeaParser parser; diff --git a/app/test/testposition.cpp b/app/test/testposition.cpp index 8bbaa05c8..4180d89b3 100644 --- a/app/test/testposition.cpp +++ b/app/test/testposition.cpp @@ -19,7 +19,7 @@ #include "position/positionkit.h" #include "position/providers/simulatedpositionprovider.h" -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -91,7 +91,7 @@ void TestPosition::simulatedPosition() QVERIFY( !positionKit->positionProvider() ); } -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS void TestPosition::testBluetoothProviderConnection() { @@ -256,7 +256,7 @@ void TestPosition::testPositionProviderKeysInSettings() QSettings rawSettings; rawSettings.remove( AppSettings::POSITION_PROVIDERS_GROUP ); // make sure nothing is there from previous tests -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS positionKit->setPositionProvider( positionKit->constructProvider( "external_bt", "AA:BB:CC:DD:EE:FF", "testProviderA" ) ); QCOMPARE( positionKit->positionProvider()->id(), "AA:BB:CC:DD:EE:FF" ); @@ -507,7 +507,7 @@ void TestPosition::testPositionTransformerAndroidPosition() PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -566,7 +566,7 @@ void TestPosition::testPositionTransformerBluetoothPosition() PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -632,7 +632,7 @@ void TestPosition::testPositionTransformerInternalAndroidPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -668,7 +668,7 @@ void TestPosition::testPositionTransformerInternalIosPosition() PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); PositionTransformer disabledTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -745,7 +745,7 @@ void TestPosition::testPositionTransformerInternalDesktopPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -780,7 +780,7 @@ void TestPosition::testPositionTransformerNetworkPosition() PositionTransformer passThroughTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, true, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -846,7 +846,7 @@ void TestPosition::testPositionTransformerSimulatedPosition() QgsCoordinateReferenceSystem geoidHeightCrs = QgsCoordinateReferenceSystem::fromEpsgId( 9707 ); PositionTransformer positionTransformer( ellipsoidHeightCrs, geoidHeightCrs, false, QgsCoordinateTransformContext() ); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS // mini file contains only minimal info like position and date QString miniNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_mini.txt"; QFile miniNmeaFile( miniNmeaPositionFilePath ); @@ -874,3 +874,99 @@ void TestPosition::testPositionTransformerSimulatedPosition() QVERIFY( qgsDoubleNear( newPosition.elevation, 127.53574931171875 ) ); QVERIFY( qgsDoubleNear( newPosition.elevation_diff, 43.764250688281265 ) ); } + +#if WITH_TRIMBLE_PROVIDERS +#include "position/providers/trimblepositionprovider.h" +#include "appsettings.h" + +void TestPosition::testTrimbleMessageParser() +{ + // Full LocationV2DataMessage sample + const QString json = QStringLiteral( + R"({ + "latitude": 48.123456, + "longitude": 17.654321, + "altitude": 150.5, + "hdop": 0.8, + "satellites": 12, + "totalSatInUse": 10, + "diffStatus": 4, + "antennaHeight": 1.234, + "targetReferenceFrameName": "ITRF2014", + "targetReferenceFrameEpoch": 2010.0 + })" ); + + GeoPosition pos = TrimblePositionProvider::parseLocationMessage( json ); + + QVERIFY( qgsDoubleNear( pos.latitude, 48.123456 ) ); + QVERIFY( qgsDoubleNear( pos.longitude, 17.654321 ) ); + QVERIFY( qgsDoubleNear( pos.elevation, 150.5 ) ); + QVERIFY( qgsDoubleNear( pos.hdop, 0.8 ) ); + QCOMPARE( pos.satellitesVisible, 12 ); + QCOMPARE( pos.satellitesUsed, 10 ); + QCOMPARE( pos.qualityIndicator, QgsGpsInformation::QualityIndicator::RTK ); + QVERIFY( qgsDoubleNear( pos.antennaHeight, 1.234 ) ); + QVERIFY( pos.antennaHeightApplied ); + + // Null / missing fields should not crash + const QString emptyJson = QStringLiteral( "{}" ); + GeoPosition emptyPos = TrimblePositionProvider::parseLocationMessage( emptyJson ); + QVERIFY( !emptyPos.hasValidPosition() ); + + // Malformed JSON + const QString badJson = QStringLiteral( "not json at all" ); + GeoPosition badPos = TrimblePositionProvider::parseLocationMessage( badJson ); + QVERIFY( !badPos.hasValidPosition() ); +} + +void TestPosition::testTrimbleFrameResolver() +{ + // Known frames should resolve to valid CRS + QgsCoordinateReferenceSystem wgs84 = TrimblePositionProvider::resolveFrame( QStringLiteral( "WGS84" ), 0 ); + QVERIFY( wgs84.isValid() ); + QCOMPARE( wgs84.authid(), QStringLiteral( "EPSG:4979" ) ); + + QgsCoordinateReferenceSystem itrf2014 = TrimblePositionProvider::resolveFrame( QStringLiteral( "ITRF2014" ), 2010.0 ); + QVERIFY( itrf2014.isValid() ); + QCOMPARE( itrf2014.authid(), QStringLiteral( "EPSG:7912" ) ); + QVERIFY( qgsDoubleNear( itrf2014.coordinateEpoch(), 2010.0 ) ); + + QgsCoordinateReferenceSystem etrs89 = TrimblePositionProvider::resolveFrame( QStringLiteral( "ETRS89" ), 0 ); + QVERIFY( etrs89.isValid() ); + QCOMPARE( etrs89.authid(), QStringLiteral( "EPSG:4936" ) ); + + // Unknown frame → WGS84 fallback + QgsCoordinateReferenceSystem unknown = TrimblePositionProvider::resolveFrame( QStringLiteral( "SomeUnknownDatum2099" ), 0 ); + QVERIFY( unknown.isValid() ); + QCOMPARE( unknown.authid(), QStringLiteral( "EPSG:4979" ) ); + + // Empty frame → WGS84 default + QgsCoordinateReferenceSystem empty = TrimblePositionProvider::resolveFrame( QString(), 0 ); + QVERIFY( empty.isValid() ); + QCOMPARE( empty.authid(), QStringLiteral( "EPSG:4979" ) ); +} + +void TestPosition::testTrimbleAntennaHeight() +{ + // Provider-supplied antenna height: display from GeoPosition, apply 0 + GeoPosition trimblePos; + trimblePos.antennaHeight = 1.5; + trimblePos.antennaHeightApplied = true; + + positionKit->parsePositionUpdate( trimblePos ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeight(), 1.5 ) ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeightToApply(), 0.0 ) ); + QVERIFY( positionKit->antennaHeightApplied() ); + + // Normal provider (no antennaHeight from stream): display from AppSettings, apply AppSettings value + GeoPosition normalPos; + normalPos.antennaHeight = -1; // not provided + normalPos.antennaHeightApplied = false; + + const double savedHeight = positionKit->appSettings() ? positionKit->appSettings()->gpsAntennaHeight() : 0.0; + positionKit->parsePositionUpdate( normalPos ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeight(), savedHeight ) ); + QVERIFY( qgsDoubleNear( positionKit->antennaHeightToApply(), savedHeight ) ); + QVERIFY( !positionKit->antennaHeightApplied() ); +} +#endif // WITH_TRIMBLE_PROVIDERS diff --git a/app/test/testposition.h b/app/test/testposition.h index 40ffd45c6..8426afa70 100644 --- a/app/test/testposition.h +++ b/app/test/testposition.h @@ -28,7 +28,7 @@ class TestPosition: public QObject void simulatedPosition(); -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS void testBluetoothProviderConnection(); void testBluetoothProviderPosition(); #endif @@ -47,6 +47,12 @@ class TestPosition: public QObject void testPositionTransformerNetworkPosition(); void testPositionTransformerSimulatedPosition(); +#if WITH_TRIMBLE_PROVIDERS + void testTrimbleMessageParser(); + void testTrimbleFrameResolver(); + void testTrimbleAntennaHeight(); +#endif + private: PositionKit *positionKit; }; diff --git a/app/test/testvariablesmanager.cpp b/app/test/testvariablesmanager.cpp index cd191c431..e3b2e74b5 100644 --- a/app/test/testvariablesmanager.cpp +++ b/app/test/testvariablesmanager.cpp @@ -7,6 +7,9 @@ * * ***************************************************************************/ #include "testvariablesmanager.h" + +#include "mmconfig.h" + #include "qgsexpression.h" #include "qgsexpressioncontext.h" #include "qgsexpressioncontextutils.h" @@ -16,7 +19,7 @@ #include "merginapi.h" -#ifdef HAVE_BLUETOOTH +#if WITH_BLUETOOTH_PROVIDERS #include "position/providers/bluetoothpositionprovider.h" #endif @@ -45,11 +48,11 @@ void TestVariablesManager::cleanup() void TestVariablesManager::testPositionVariables() { -#ifdef HAVE_BLUETOOTH mAppSettings->setGpsAntennaHeight( 0 ); - BluetoothPositionProvider *btProvider = new BluetoothPositionProvider( "AA:AA:FF:AA:00:10", "testBluetoothProvider" ); - mPositionKit->setPositionProvider( btProvider ); + AbstractPositionProvider *testProvider = mPositionKit->constructProvider( QStringLiteral( "internal" ), + QStringLiteral( "devicegps" ) ); + mPositionKit->setPositionProvider( testProvider ); NmeaParser parser; QString fullNmeaPositionFilePath = TestUtils::testDataDir() + "/position/nmea_petrzalka_full.txt"; @@ -60,7 +63,7 @@ void TestVariablesManager::testPositionVariables() GeoPosition pos = GeoPosition::fromQgsGpsInformation( position ); pos.verticalSpeed = 0; pos.magneticVariation = 0; - emit btProvider->positionChanged( pos ); + emit testProvider->positionChanged( pos ); QgsExpressionContext context; context << mVariablesManager->positionScope(); @@ -82,14 +85,14 @@ void TestVariablesManager::testPositionVariables() evaluateExpression( QStringLiteral( "@position_pdop" ), QStringLiteral( "5.90" ), &context ); evaluateExpression( QStringLiteral( "@position_gps_fix" ), QStringLiteral( "RTK float" ), &context ); 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_bt" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_address" ), QStringLiteral( "devicegps" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_name" ), QStringLiteral( "Internal" ), &context ); + evaluateExpression( QStringLiteral( "@position_provider_type" ), QStringLiteral( "internal" ), &context ); mAppSettings->setGpsAntennaHeight( 1.6784 ); pos.verticalSpeed = 1.345; pos.magneticVariation = 14.34; - emit btProvider->positionChanged( pos ); + emit testProvider->positionChanged( pos ); context << mVariablesManager->positionScope(); evaluateExpression( QStringLiteral( "@position_vertical_speed" ), QStringLiteral( "1.34" ), &context ); @@ -97,7 +100,6 @@ void TestVariablesManager::testPositionVariables() evaluateExpression( QStringLiteral( "@position_gps_antenna_height" ), QStringLiteral( "1.678" ), &context ); mAppSettings->setGpsAntennaHeight( 0 ); -#endif } void TestVariablesManager::testUserVariables() diff --git a/cmake_templates/iOSInfo.plist.in b/cmake_templates/iOSInfo.plist.in index 058f409f1..f85db7a9f 100644 --- a/cmake_templates/iOSInfo.plist.in +++ b/cmake_templates/iOSInfo.plist.in @@ -26,6 +26,17 @@ MinimumOSVersion ${IPHONEOS_DEPLOYMENT_TARGET} + CFBundleURLTypes + + + CFBundleURLName + uk.co.lutraconsulting.merginmaps + CFBundleURLSchemes + + merginmaps + + + NSCameraUsageDescription Program requires access to camera to take pictures for features on map NSLocationAlwaysAndWhenInUseUsageDescription diff --git a/cmake_templates/mmconfig.h.in b/cmake_templates/mmconfig.h.in index 181bfc603..3255029e5 100644 --- a/cmake_templates/mmconfig.h.in +++ b/cmake_templates/mmconfig.h.in @@ -11,7 +11,8 @@ #cmakedefine MM_TEST #cmakedefine TEST_DATA_DIR "@TEST_DATA_DIR@" -#cmakedefine HAVE_BLUETOOTH +#cmakedefine01 WITH_BLUETOOTH_PROVIDERS +#cmakedefine01 WITH_TRIMBLE_PROVIDERS #endif diff --git a/vcpkg.json b/vcpkg.json index 7c585b254..05878aed5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -111,6 +111,10 @@ ] }, "qtremoteobjects", + { + "name": "qtwebsockets", + "platform": "android | ios" + }, { "name": "qtsensors", "features": [