diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index b8f8e12..b229dc1 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -1,104 +1,109 @@ ecm_setup_version(PROJECT VARIABLE_PREFIX KIROGI PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KirogiCoreConfigVersion.cmake" SOVERSION ${KIROGI_VERSION_STRING} ) set(kirogicore_SRCS abstractvehicle.cpp vehiclesupportplugin.cpp vehiclesupportpluginmodel.cpp ) ecm_qt_declare_logging_category(kirogicore_SRCS HEADER debug.h IDENTIFIER KIROGI_CORE CATEGORY_NAME "kirogi.core" ) ecm_generate_headers(Kirogi_CamelCase_HEADERS HEADER_NAMES AbstractVehicle VehicleSupportPlugin VehicleSupportPluginModel REQUIRED_HEADERS Kirogi_HEADERS PREFIX kirogi ) +add_subdirectory(positionsource) + add_library(kirogicore SHARED ${kirogicore_SRCS} ${Kirogi_HEADERS}) add_library(KirogiCore ALIAS kirogicore) generate_export_header(kirogicore BASE_NAME Kirogi EXPORT_FILE_NAME kirogicore_export.h) target_include_directories(kirogicore INTERFACE "$") target_link_libraries(kirogicore PRIVATE Qt5::Core Qt5::Positioning Qt5::Qml Qt5::Quick KF5::CoreAddons + positionsource ) set_target_properties(kirogicore PROPERTIES VERSION ${KIROGI_VERSION_STRING} SOVERSION ${KIROGI_SOVERSION} EXPORT_NAME KirogiCore OUTPUT_NAME kirogicore ) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") target_compile_options(kirogicore PRIVATE -pedantic) endif() install(TARGETS kirogicore EXPORT kirogicoreLibraryTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${Kirogi_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kirogicore_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/Kirogi/kirogi COMPONENT Devel) install(FILES ${Kirogi_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR}/Kirogi/Kirogi COMPONENT Devel) write_basic_config_version_file(${CMAKE_CURRENT_BINARY_DIR}/KirogiCoreConfigVersion.cmake VERSION "${PROJECT_VERSION}" COMPATIBILITY AnyNewerVersion) set(CMAKECONFIG_INSTALL_DIR ${KDE_INSTALL_LIBDIR}/cmake/KirogiCore) configure_package_config_file(KirogiCoreConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/KirogiCoreConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KirogiCoreConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/KirogiCoreConfigVersion.cmake DESTINATION ${CMAKECONFIG_INSTALL_DIR}) install(EXPORT kirogicoreLibraryTargets DESTINATION ${CMAKECONFIG_INSTALL_DIR} FILE KirogiCoreLibraryTargets.cmake) install(FILES kirogivehiclesupportplugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR}) install(FILES kirogi.categories DESTINATION ${KDE_INSTALL_CONFDIR}) if(NOT BUILD_QT_QUICK_LIB) return() endif() add_library(kirogiqtquickplugin SHARED qtquickplugin.cpp) target_link_libraries(kirogiqtquickplugin Qt5::Positioning Qt5::Qml Qt5::Quick - KirogiCore) + KirogiCore + positionsource + ) install(TARGETS kirogiqtquickplugin DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirogi) install(FILES qmldir DESTINATION ${KDE_INSTALL_QMLDIR}/org/kde/kirogi) diff --git a/src/lib/positionsource/CMakeLists.txt b/src/lib/positionsource/CMakeLists.txt new file mode 100644 index 0000000..d47af02 --- /dev/null +++ b/src/lib/positionsource/CMakeLists.txt @@ -0,0 +1,27 @@ +set(positionsource_SRCS + positionsource.cpp +) + +ecm_qt_declare_logging_category(positionsource_SRCS + HEADER debug.h + IDENTIFIER POSITIONSOURCE + CATEGORY_NAME "kirogi.positionsource" +) + +add_library( + positionsource +STATIC + ${positionsource_SRCS} +) + +target_link_libraries(positionsource + PRIVATE + Qt5::Core + Qt5::Network + Qt5::Positioning + Qt5::Quick +) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(positionsource PRIVATE -pedantic) +endif() diff --git a/src/lib/positionsource/positionsource.cpp b/src/lib/positionsource/positionsource.cpp new file mode 100644 index 0000000..4693b2c --- /dev/null +++ b/src/lib/positionsource/positionsource.cpp @@ -0,0 +1,92 @@ +#include "positionsource.h" + +#include "debug.h" + +#include + +PositionSource::PositionSource() +{ + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + + createPositionSource(); +} + +void PositionSource::createPositionSource() +{ + // The smartpointer will take care of the pointer, that's why we are using nullptr as parent. + m_geoPositionSource.reset(QGeoPositionInfoSource::createDefaultSource(nullptr)); + if (m_geoPositionSource.isNull()) { + qCWarning(POSITIONSOURCE) << "Position source is not available for this device."; + return; + } + + qCDebug(POSITIONSOURCE) << "New position source:" << m_geoPositionSource->sourceName(); + + // Set the update interval to 3s + m_geoPositionSource->setUpdateInterval(3000); + m_geoPositionSource->setPreferredPositioningMethods(QGeoPositionInfoSource::SatellitePositioningMethods); + m_geoPositionSource->startUpdates(); + + connect(m_geoPositionSource.get(), QOverload::of(&QGeoPositionInfoSource::error), this, &PositionSource::setPositionSourceError); + connect(m_geoPositionSource.get(), &QGeoPositionInfoSource::positionUpdated, this, &PositionSource::setPositionInfo); +} + +bool PositionSource::enabled() const +{ + return m_enabled; +} + +void PositionSource::setEnabled(bool enabled) +{ + if (m_enabled == enabled) { + return; + } + + m_enabled = enabled; + emit enabledChanged(m_enabled); +} + +void PositionSource::setPositionInfo(const QGeoPositionInfo &geoPositionInfo) +{ + if (!geoPositionInfo.isValid()) { + qCWarning(POSITIONSOURCE) << "Not valid position info from position source:" << geoPositionInfo; + return; + } + + const auto geoCoordinate = geoPositionInfo.coordinate(); + if (m_geoCoordinate == geoCoordinate) { + return; + } + + m_geoCoordinate = geoCoordinate; + emit coordinateChanged(m_geoCoordinate); +} + +QGeoCoordinate PositionSource::coordinate() const +{ + return m_geoCoordinate; +} + +const QGeoPositionInfoSource *PositionSource::positionInfoSource() const +{ + return m_geoPositionSource.get(); +} + +void PositionSource::setPositionSourceError(QGeoPositionInfoSource::Error positioningError) +{ + qWarning(POSITIONSOURCE) << "Position source error:" << positioningError; +} + +QObject *PositionSource::qmlSingletonRegister(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + + return &self(); +} + +PositionSource &PositionSource::self() +{ + static PositionSource self; + return self; +} diff --git a/src/lib/positionsource/positionsource.h b/src/lib/positionsource/positionsource.h new file mode 100644 index 0000000..bcf2e9a --- /dev/null +++ b/src/lib/positionsource/positionsource.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include + +class QJSEngine; +class QQmlEngine; + +/** + * @brief Manage the position source of the ground control station + * + */ +class PositionSource : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(QGeoCoordinate coordinate READ coordinate NOTIFY coordinateChanged) + +public: + /** + * @brief Return PositionSource status, if it's enabled + * + * @return true PositionSource is enable and working + * @return false Positionsource is disabled + */ + bool enabled() const; + + /** + * @brief Turn PositionSource to enable or disable + * + * @param enabled + */ + void setEnabled(bool enabled); + + /** + * @brief Set a new position info + * + * @param geoPositionInfo + */ + void setPositionInfo(const QGeoPositionInfo &geoPositionInfo); + + /** + * @brief Return the last valid coordinate + * + * @return QGeoCoordinate + */ + QGeoCoordinate coordinate() const; + + /** + * @brief Return the position source for this class + * + * @return const QGeoPositionInfoSource* + */ + const QGeoPositionInfoSource *positionInfoSource() const; + + /** + * @brief Return PositionSource pointer + * + * @return PositionSource* + */ + static PositionSource &self(); + + /** + * @brief Return a pointer of this singleton to the qml register function + * + * @param engine + * @param scriptEngine + * @return QObject* + */ + static QObject *qmlSingletonRegister(QQmlEngine *engine, QJSEngine *scriptEngine); + +Q_SIGNALS: + void enabledChanged(bool enabled); + void coordinateChanged(const QGeoCoordinate &geoCoordinate); + +private: + Q_DISABLE_COPY(PositionSource) + /** + * @brief Construct a new Position Source object + * + */ + PositionSource(); + + /** + * @brief Create the position source for this class + * + */ + void createPositionSource(); + + /** + * @brief Set position source error type + * + * @param positioningError + */ + void setPositionSourceError(QGeoPositionInfoSource::Error positioningError); + + bool m_enabled; + QGeoCoordinate m_geoCoordinate; + QSharedPointer m_geoPositionSource; +}; diff --git a/src/lib/qtquickplugin.cpp b/src/lib/qtquickplugin.cpp index 37cb8fd..de5eedd 100644 --- a/src/lib/qtquickplugin.cpp +++ b/src/lib/qtquickplugin.cpp @@ -1,40 +1,43 @@ /* * Copyright 2019 Eike Hein * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "qtquickplugin.h" #include "abstractvehicle.h" #include "vehiclesupportplugin.h" #include "vehiclesupportpluginmodel.h" +#include "positionsource/positionsource.h" + #include namespace Kirogi { void QtQuickPlugin::registerTypes(const char *uri) { Q_ASSERT(uri == QLatin1String("org.kde.kirogi")); qmlRegisterUncreatableType(uri, 0, 1, "AbstractVehicle", "AbstractVehicle cannot be created from QML."); qmlRegisterType(uri, 0, 1, "VehicleSupportPluginModel"); -} + qmlRegisterSingletonType(uri, 0, 1, "PositionSource", PositionSource::qmlSingletonRegister); +} } diff --git a/src/ui/FlightControls.qml b/src/ui/FlightControls.qml index 18c49b5..4344ce5 100644 --- a/src/ui/FlightControls.qml +++ b/src/ui/FlightControls.qml @@ -1,959 +1,959 @@ /* * Copyright 2019 Eike Hein * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.12 import QtQuick.Controls 2.12 as QQC2 import org.kde.kirigami 2.6 as Kirigami import org.freedesktop.gstreamer.GLVideoItem 1.0 import org.kde.kirogi 0.1 as Kirogi Kirigami.Page { id: page LayoutMirroring.enabled: false LayoutMirroring.childrenInherit: true readonly property int yardstick: Math.min(parent.width, parent.height) readonly property bool touched: leftTouchPoint.active || rightTouchPoint.active property alias gamepad: gamepadLoader.item leftPadding: 0 rightPadding: 0 topPadding: 0 bottomPadding: 0 Connections { target: kirogi onCurrentPageChanged: updatePilotingState() onCurrentVehicleChanged: updatePilotingState() onReadyChanged: updatePilotingState() } function updatePilotingState() { var vehicle = kirogi.currentVehicle; if (!vehicle || !vehicle.ready) { return; } vehicle.setPiloting(kirogi.currentPage == page); if (kirogi.currentPage == page && !kirogi.currentVehicle.videoStreamEnabled && vehicle.isActionSupported(Kirogi.AbstractVehicle.ToggleVideoStream)) { vehicle.requestEnableVideoStream(true); } } Image { id: cameraStream anchors.fill: parent fillMode: Image.PreserveAspectCrop smooth: true source: "fallback.jpg" Connections { target: kirogi onCurrentVehicleChanged: { videoPlayer.playing = kirogi.currentVehicle != null; } onCurrentPageChanged: { videoPlayer.playing = (kirogi.currentPage == page || kirogi.currentVehicle); } } Binding { target: videoPlayer property: "pipeline" value: kirogi.currentVehicle ? kirogi.currentVehicle.videoSource : "" } GstGLVideoItem { anchors.centerIn: parent width: parent.width height: parent.width/1.77 // FIXME: Work with non-16:9 videos. objectName: "videoOutput" } Rectangle { id: videoOverlay anchors.fill: parent visible: !kirogi.currentVehicle || kirogi.currentVehicle.videoSource === "" opacity: 0.4 color: "black" } } // FIXME TODO: This is a workaround around the org.kde.desktop+Breeze style engine // hijacking drag on the window. TapHandler { enabled: !Kirigami.Settings.isMobile } Item { anchors.left: parent.left width: parent.width / 2 height: parent.height PointHandler { id: leftTouchPoint enabled: inputMode.selectedMode == 0 || kirogiSettings.alwaysShowDPads grabPermissions: PointerHandler.ApprovesTakeOverByAnything | PointerHandler.ApprovesCancellation } } Item { anchors.right: parent.right width: parent.width / 2 height: parent.height PointHandler { id: rightTouchPoint enabled: inputMode.selectedMode == 0 || kirogiSettings.alwaysShowDPads } } TouchButton { id: leftButton anchors.top: leftPillBox.bottom anchors.topMargin: Math.round(leftPillBox.y * 1.5) anchors.left: parent.left anchors.leftMargin: leftPillBox.y background: cameraStream icon: kirogi.currentVehicle ? kirogi.currentVehicle.iconName : "uav" toolTipText: i18nc("%1 = Keyboard shortcut", "Drone (%1)", vehiclePageAction.__shortcut.nativeText) onTapped: switchApplicationPage(vehiclePage) } PillBox { id: launchButton anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: Kirigami.Units.smallSpacing width: Math.min(launchButtonLabel.implicitWidth + Kirigami.Units.smallSpacing * 4, rightPillBox.x - leftPillBox.x - leftPillBox.width - (leftPillBox.x * 2)) height: 2 * Math.round((leftPillBox.height * 1.12) / 2); readonly property var __color: { if (launchButtonMouseArea.containsMouse) { return Kirigami.Theme.hoverColor; } if (kirogi.connected) { if (kirogi.flying) { return "red"; } else if (kirogi.ready) { return "green"; } else { return "yellow"; } } return "red"; } background: cameraStream backgroundColor: "dark" + __color backgroundOpacity: 0.4 borderWidth: 2 borderRadius: height / 4 borderColor: launchButtonLabel.color Text { id: launchButtonLabel anchors.fill: parent font.pixelSize: parent.height * 0.7 font.bold: true color: launchButton.__color horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter elide: Text.ElideRight text: { if (kirogi.connected) { if (kirogi.flying) { return i18n("LAND"); } else if (kirogi.ready) { return i18n("TAKE OFF"); } else { return i18n("PREPARING") } } return i18n("DISCONNECTED"); } Behavior on color { enabled: !launchButtonMouseArea.pressed ColorAnimation { duration: Kirigami.Units.shortDuration } } } MouseArea { id: launchButtonMouseArea anchors.fill: parent enabled: kirogi.ready hoverEnabled: enabled onClicked: { if (kirogi.flying) { kirogi.currentVehicle.requestLand(); } else { kirogi.currentVehicle.requestTakeOff(); } } } } TouchButton { id: rightButton anchors.top: rightPillBox.bottom anchors.topMargin: leftButton.anchors.topMargin anchors.right: parent.right anchors.rightMargin: leftButton.anchors.leftMargin background: cameraStream icon: "map-flat" toolTipText: i18nc("%1 = Keyboard shortcut", "Navigation Map (%1)", navigationMapPageAction.__shortcut.nativeText) onTapped: switchApplicationPage(navigationMapPage) } ModeRocker { id: flightMode enabled: kirogi.ready anchors.left: parent.left anchors.verticalCenter: shotButton.verticalCenter background: cameraStream selectedMode: { if (kirogi.ready) { if (kirogi.currentVehicle.performanceMode == Kirogi.AbstractVehicle.FilmPerformance) { return 0; } else if (kirogi.currentVehicle.performanceMode == Kirogi.AbstractVehicle.SportPerformance) { return 1; } } return -1; } firstLabelText: i18n("FILM") firstToolTipText: i18n("Fly Slow") secondLabelText: i18n("SPORT") secondToolTipText: i18n("Fly Fast") onModeTapped: { if (selectedMode == mode) { return; } if (mode == 0) { kirogi.currentVehicle.requestPerformanceMode(Kirogi.AbstractVehicle.FilmPerformance); } else if (mode == 1) { kirogi.currentVehicle.requestPerformanceMode(Kirogi.AbstractVehicle.SportPerformance); } } Component.onCompleted: { var handleWidth = kirogi.LayoutMirroring.enabled ? parent.width - globalDrawer.handle.x : globalDrawer.handle.x + globalDrawer.handle.width; anchors.leftMargin = globalDrawer.modal ? handleWidth + leftPillBox.y : leftPillBox.y; } } ModeRocker { id: inputMode visible: gamepad && gamepad.connected && !kirogiSettings.alwaysShowDPads anchors.verticalCenter: shotButton.verticalCenter anchors.left: flightMode.right anchors.leftMargin: leftPillBox.y firstLabelText: i18n("SCREEN") firstIconSource: Kirigami.Settings.isMobile ? "phone-symbolic" : "computer-symbolic" firstToolTipText: i18n("Use Virtual D-Pads") secondLabelText: i18n("CONTROLLER") secondIconSource: "folder-games-symbolic" secondToolTipText: i18n("Use Gamepad Controller") showLabels: false showIcons: true selectedMode: kirogiSettings.lastInputMode onModeTapped: { selectedMode = mode; kirogiSettings.lastInputMode = selectedMode; kirogiSettings.save(); } } ModeRocker { id: shotMode enabled: kirogi.ready anchors.right: shotButton.right anchors.rightMargin: shotButton.width / 2 anchors.verticalCenter: shotButton.verticalCenter width: (implicitWidth + shotButton.width / 2) - Kirigami.Units.largeSpacing property int requestedMode: 0 background: cameraStream firstModeEnabled: enabled && kirogi.currentVehicle.isActionSupported(Kirogi.AbstractVehicle.RecordVideo) secondModeEnabled: enabled && kirogi.currentVehicle.isActionSupported(Kirogi.AbstractVehicle.TakePicture) firstLabelText: i18n("VIDEO") firstIconSource: "emblem-videos-symbolic" firstToolTipText: i18n("Record Videos") secondLabelText: i18n("PHOTO") secondIconSource: "emblem-photos-symbolic" secondToolTipText: i18n("Take Photos") showLabels: false showIcons: true selectedMode: kirogi.ready ? requestedMode : -1 onModeTapped: requestedMode = mode } TouchButton { id: shotButton enabled: { if (!kirogi.ready) { return false; } if ((shotMode.selectedMode == 0 && !kirogi.currentVehicle.isActionSupported(Kirogi.AbstractVehicle.RecordVideo)) || (shotMode.selectedMode == 1 && (!kirogi.currentVehicle.isActionSupported(Kirogi.AbstractVehicle.TakePicture) || !kirogi.currentVehicle.canTakePicture))) { return false; } return true; } anchors.right: parent.right anchors.rightMargin: flightMode.anchors.leftMargin anchors.bottom: parent.bottom anchors.bottomMargin: launchButton.anchors.topMargin background: cameraStream icon: "media-record-symbolic" iconColor: shotMode.selectedMode == 0 && (kirogi.currentVehicle && kirogi.currentVehicle.isRecordingVideo) ? "red" : "white" toolTipText: { if (shotMode.selectedMode) { return i18n("Take Photo"); } else if (kirogi.currentVehicle && kirogi.currentVehicle.isRecordingVideo) { return i18n("Stop Recording Video"); } return i18n("Record Video"); } onTapped: { if (!kirogi.ready) { return; } if (shotMode.selectedMode == 0) { kirogi.currentVehicle.requestAction(Kirogi.AbstractVehicle.RecordVideo); } else if (shotMode.selectedMode == 1) { kirogi.currentVehicle.requestAction(Kirogi.AbstractVehicle.TakePicture); } } } TouchDPad { id: leftDPad visible: inputMode.selectedMode == 0 || kirogiSettings.alwaysShowDPads || (gamepad && !gamepad.connected) anchors.left: parent.left anchors.leftMargin: yardstick * 0.18 anchors.bottom: parent.bottom anchors.bottomMargin: yardstick * 0.20 width: Math.min(yardstick * 0.45, parent.width / 4) height: width background: cameraStream leftIcon: "edit-undo" leftToolTipText: i18n("Turn Left") rightIcon: "edit-redo" rightToolTipText: i18n("Turn Right") topIcon: "arrow-up" topToolTipText: i18n("Move Up") bottomIcon: "arrow-down" bottomToolTipText: i18n("Move Down") onXChanged: moved = aboutToMove onAxisXChanged: { kirogi.currentVehicle.pilot(rightDPad.axisX, rightDPad.axisY, leftDPad.axisX, leftDPad.axisY); } onAxisYChanged: { kirogi.currentVehicle.pilot(rightDPad.axisX, rightDPad.axisY, leftDPad.axisX, leftDPad.axisY); } touchPos: { if (moved && leftTouchPoint) { var xDifference = 0; if (leftTouchPoint.point.scenePosition.x > leftTouchPoint.point.scenePressPosition.x) { xDifference = xDifference + Math.abs(leftTouchPoint.point.scenePressPosition.x - leftTouchPoint.point.scenePosition.x); } else { xDifference = xDifference - Math.abs(leftTouchPoint.point.scenePressPosition.x - leftTouchPoint.point.scenePosition.x) } var x = leftDPad.x + leftDPad.width / 2 + xDifference; var yDifference = 0; if (leftTouchPoint.point.scenePosition.y > leftTouchPoint.point.scenePressPosition.y) { yDifference = yDifference + Math.abs(leftTouchPoint.point.scenePressPosition.y - leftTouchPoint.point.scenePosition.y); } else { yDifference = yDifference - Math.abs(leftTouchPoint.point.scenePressPosition.y - leftTouchPoint.point.scenePosition.y) } var y = leftDPad.y + leftDPad.height / 2 + yDifference; return parent.mapToItem(leftDPad, x, y); } return null; } states: [ State { name: "inactive" AnchorChanges { target: leftDPad anchors.left: parent.left anchors.bottom: parent.bottom } PropertyChanges { target: leftDPad aboutToMove: false moved: false } }, State { name: "active" when: leftTouchPoint.active AnchorChanges { target: leftDPad anchors.left: undefined anchors.bottom: undefined } PropertyChanges { target: leftDPad aboutToMove: true x: Math.min((parent.width / 2) - width, Math.max(0, leftTouchPoint.point.scenePressPosition.x - width / 2)) y: Math.min(parent.height - height, Math.max(0, leftTouchPoint.point.scenePressPosition.y - height / 2)) } } ] } TouchDPad { id: rightDPad visible: leftDPad.visible width: leftDPad.height height: width anchors.right: parent.right anchors.rightMargin: leftDPad.anchors.leftMargin anchors.bottom: parent.bottom anchors.bottomMargin: leftDPad.anchors.bottomMargin background: cameraStream leftIcon: "go-previous" leftToolTipText: i18n("Move Left") rightIcon: "go-next" rightToolTipText: i18n("Move Right") topIcon: "go-up" topToolTipText: i18n("Move Forward") bottomIcon: "go-down" bottomToolTipText: i18n("Move Backward") onXChanged: moved = aboutToMove onAxisXChanged: { kirogi.currentVehicle.pilot(rightDPad.axisX, rightDPad.axisY, leftDPad.axisX, leftDPad.axisY); } onAxisYChanged: { kirogi.currentVehicle.pilot(rightDPad.axisX, rightDPad.axisY, leftDPad.axisX, leftDPad.axisY); } touchPos: { if (moved && rightTouchPoint.active) { var xDifference = 0; if (rightTouchPoint.point.scenePosition.x > rightTouchPoint.point.scenePressPosition.x) { xDifference = xDifference + Math.abs(rightTouchPoint.point.scenePressPosition.x - rightTouchPoint.point.scenePosition.x); } else { xDifference = xDifference - Math.abs(rightTouchPoint.point.scenePressPosition.x - rightTouchPoint.point.scenePosition.x) } var x = rightDPad.x + rightDPad.width / 2 + xDifference; var yDifference = 0; if (rightTouchPoint.point.scenePosition.y > rightTouchPoint.point.scenePressPosition.y) { yDifference = yDifference + Math.abs(rightTouchPoint.point.scenePressPosition.y - rightTouchPoint.point.scenePosition.y); } else { yDifference = yDifference - Math.abs(rightTouchPoint.point.scenePressPosition.y - rightTouchPoint.point.scenePosition.y) } var y = rightDPad.y + rightDPad.height / 2 + yDifference; return parent.mapToItem(rightDPad, x, y); } return null; } states: [ State { name: "inactive" AnchorChanges { target: rightDPad anchors.left: parent.right anchors.bottom: parent.bottom } PropertyChanges { target: rightDPad aboutToMove: false moved: false } }, State { name: "active" when: rightTouchPoint.active AnchorChanges { target: rightDPad anchors.right: undefined anchors.bottom: undefined } PropertyChanges { target: rightDPad aboutToMove: true x: Math.max(parent.width / 2, Math.min(parent.width - width, rightTouchPoint.point.scenePressPosition.x - width / 2)) y: Math.min(parent.height - height, Math.max(0, rightTouchPoint.point.scenePressPosition.y - height / 2)) } } ] } PillBox { id: leftPillBox anchors.verticalCenter: launchButton.verticalCenter anchors.left: parent.left anchors.leftMargin: y width: leftPillBoxContents.implicitWidth + Kirigami.Units.largeSpacing * 4 height: 2 * Math.round((Math.max(Kirigami.Units.iconSizes.small, fontMetrics.height) + Kirigami.Units.smallSpacing * 3) / 2); background: cameraStream Row { id: leftPillBoxContents anchors.horizontalCenter: parent.horizontalCenter height: parent.height spacing: Kirigami.Units.largeSpacing Kirigami.Icon { anchors.verticalCenter: parent.verticalCenter width: Kirigami.Units.iconSizes.small height: width color: "white" smooth: true isMask: true source: "speedometer" } QQC2.Label { anchors.verticalCenter: parent.verticalCenter width: kirogi.currentVehicle ? Math.round(Math.max(implicitWidth, fontMetrics.tightBoundingRect(i18n("%1 m/s", "0.0")).width)) : Math.round(implicitWidth) color: "white" horizontalAlignment: Text.AlignRight text: { if(kirogi.currentVehicle) { return i18n("%1 m/s", kirogi.flying ? kirogi.currentVehicle.speed : "0"); } return i18n("– m/s"); } } PillBoxSeparator {} Kirigami.Icon { anchors.verticalCenter: parent.verticalCenter width: Kirigami.Units.iconSizes.small height: width color: "white" smooth: true isMask: true source: "kruler-west" } QQC2.Label { anchors.verticalCenter: parent.verticalCenter width: kirogi.ready ? Math.round(Math.max(implicitWidth, fontMetrics.tightBoundingRect(i18n("%1 m", "0.0")).width)) : Math.round(implicitWidth) color: "white" horizontalAlignment: Text.AlignRight text: { if (kirogi.currentVehicle) { return i18n("%1 m", kirogi.currentVehicle.altitude.toFixed(2)) } return i18n("– m"); } } PillBoxSeparator {} Kirigami.Icon { anchors.verticalCenter: parent.verticalCenter width: Kirigami.Units.iconSizes.small height: width color: "white" smooth: true isMask: true source: "kruler-south" } QQC2.Label { anchors.verticalCenter: parent.verticalCenter width: kirogi.currentVehicle ? Math.round(Math.max(implicitWidth, fontMetrics.tightBoundingRect(i18n("%1 m", "0.0")).width)) : Math.round(implicitWidth) color: "white" horizontalAlignment: Text.AlignRight text: { if (kirogi.currentVehicle && kirogi.currentVehicle.distance >= 0) { return i18n("%1 m", kirogi.currentVehicle.distance.toFixed(1)); } - if (gpsPosition.distance !== 0) { - return i18n("%1 m", gpsPosition.distance.toFixed(1)); + if (kirogi.distance !== 0) { + return i18n("%1 m", kirogi.distance.toFixed(1)); } return i18n("– m"); } } } } PillBox { id: rightPillBox anchors.verticalCenter: launchButton.verticalCenter anchors.right: parent.right anchors.rightMargin: leftPillBox.anchors.leftMargin width: rightPillBoxContents.implicitWidth + Kirigami.Units.largeSpacing * 4 height: leftPillBox.height background: cameraStream Row { id: rightPillBoxContents x: Kirigami.Units.largeSpacing anchors.horizontalCenter: parent.horizontalCenter height: parent.height spacing: Kirigami.Units.largeSpacing Kirigami.Icon { anchors.verticalCenter: parent.verticalCenter width: Kirigami.Units.iconSizes.small height: width color: "white" smooth: true isMask: true source: "clock" } QQC2.Label { anchors.verticalCenter: parent.verticalCenter width: kirogi.currentVehicle ? Math.round(Math.max(implicitWidth, fontMetrics.tightBoundingRect(i18n("%1 m", "0:00")).width)) : Math.round(implicitWidth) color: "white" horizontalAlignment: Text.AlignRight text: { if (kirogi.ready) { var time = kirogi.flying ? kirogi.currentVehicle.flightTime : 0; return i18n("%1 min", (time - (time %= 60)) / 60 + (9 < time ?':':':0') + time); } return i18n("– min"); } } PillBoxSeparator {} Kirigami.Icon { anchors.verticalCenter: parent.verticalCenter width: Kirigami.Units.iconSizes.small height: width color: "white" smooth: true isMask: kirogi.currentVehicle source: { if (kirogi.currentVehicle) { if (kirogi.currentVehicle.signalStrength === 0) { return "network-wireless-connected-00"; } else if (kirogi.currentVehicle.signalStrength < 25) { return "network-wireless-connected-25"; } else if (kirogi.currentVehicle.signalStrength < 50) { return "network-wireless-connected-50"; } else if (kirogi.currentVehicle.signalStrength < 75) { return "network-wireless-connected-75"; } else if (kirogi.currentVehicle.signalStrength <= 100) { return "network-wireless-connected-100"; } } if (kirogi.connected) { return "network-wireless-acquiring"; } return "network-wireless-disconnected"; } } QQC2.Label { anchors.verticalCenter: parent.verticalCenter width: kirogi.currentVehicle ? Math.round(Math.max(implicitWidth, fontMetrics.tightBoundingRect(i18n("%1%", 00)).width)) : Math.round(implicitWidth) color: "white" horizontalAlignment: Text.AlignRight text: kirogi.currentVehicle ? i18n("%1%", kirogi.currentVehicle.signalStrength) : i18n("N/A") } PillBoxSeparator {} Kirigami.Icon { anchors.verticalCenter: parent.verticalCenter width: Kirigami.Units.iconSizes.small height: width color: "white" smooth: true isMask: kirogi.currentVehicle source: { if (kirogi.currentVehicle) { var roundedBatteryLevel = Math.round(kirogi.currentVehicle.batteryLevel / 10); return "battery-" + roundedBatteryLevel.toString().padStart(2, "0") + "0"; } return "battery-missing"; } } QQC2.Label { anchors.verticalCenter: parent.verticalCenter width: kirogi.currentVehicle ? Math.round(Math.max(implicitWidth, fontMetrics.tightBoundingRect(i18n("%1%", 00)).width)) : Math.round(implicitWidth) color: "white" horizontalAlignment: Text.AlignRight text: kirogi.currentVehicle ? i18n("%1%", kirogi.currentVehicle.batteryLevel) : i18n("N/A") } } } PitchBar { id: pitchBar anchors.centerIn: parent width: yardstick * 0.04 height: parent.height * 0.6 pitch: kirogi.currentVehicle ? kirogi.currentVehicle.pitch * (180/Math.PI) : 0.0 } VirtualHorizon { id: virtualHorizon anchors.centerIn: pitchBar width: yardstick * 0.2 roll: kirogi.currentVehicle ? kirogi.currentVehicle.roll * (180/Math.PI) : 0 } YawBar { id: yawBar tickWidth: 10 anchors.left: leftDPad.horizontalCenter anchors.right: rightDPad.horizontalCenter anchors.bottom: inputMode.visible ? inputMode.top : parent.bottom yaw: kirogi.currentVehicle ? kirogi.currentVehicle.yaw * (180 / Math.PI) : 0 } VehicleActionsDrawer { enabled: kirogi.currentPage == page // FIXME TODO: Why doesn't come down from page.enabled? width: Kirigami.Units.gridUnit * 19 edge: kirogi.LayoutMirroring.enabled ? Qt.LeftEdge : Qt.RightEdge handleClosedIcon.source: "configure" } Loader { id: gamepadLoader source: "Gamepad.qml" asynchronous: true // FIXME TODO: QtGamepad currently causes performance problems on // Android (blocking multi-tasking) that need to be investigated. active: !Kirigami.Settings.isMobile } Component.onCompleted: videoPlayer.playing = kirogiSettings.flying } diff --git a/src/ui/main.qml b/src/ui/main.qml index f82d0ce..32ccfa3 100644 --- a/src/ui/main.qml +++ b/src/ui/main.qml @@ -1,271 +1,253 @@ /* * Copyright 2019 Eike Hein * * 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) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 as QQC2 import QtPositioning 5.12 import org.kde.kirigami 2.6 as Kirigami import org.kde.kirogi 0.1 as Kirogi Kirigami.ApplicationWindow { id: kirogi width: 800 height: 450 property QtObject currentVehicle: null readonly property bool connected: currentVehicle && currentVehicle.connected readonly property bool ready: currentVehicle && currentVehicle.ready readonly property bool flying: currentVehicle && currentVehicle.flying property var currentPage: pageStack.currentItem property alias currentPlugin: pluginModel.currentPlugin property alias currentPluginName: pluginModel.currentPluginName - property alias position: gpsPosition._lastKnownCoordinate + + readonly property var position: Kirogi.PositionSource.coordinate + readonly property real distance: { + if (!position || !position.isValid) { + return 0.0; + } + + // If currentVehicle is null or there is no valid position + // set distance to zero. + if (!currentVehicle || !currentVehicle.gpsPosition.isValid) { + return 0.0; + } + + return position.distanceTo(currentVehicle.gpsPosition); + } + + onPositionChanged: { + // Position via internet IP does not provide a valid altitude. + if (position.isValid && currentVehicle) { + currentVehicle.setControllerGpsPosition(position) + } + } pageStack.interactive: false pageStack.defaultColumnWidth: width pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.None globalDrawer: Kirigami.GlobalDrawer { title: i18n("Kirogi") titleIcon: "kirogi" width: Kirigami.Units.gridUnit * 14 actions: [ Kirigami.Action { id: vehiclePageAction checked: currentPage == vehiclePage iconName: "uav-quadcopter" text: i18n("Drone") tooltip: "Alt+1" shortcut: tooltip onTriggered: switchApplicationPage(vehiclePage) }, Kirigami.Action { id: flightControlsPageAction checked: currentPage == flightControlsPage iconName: "transform-move" text: i18n("Flight Controls") tooltip: "Alt+2" shortcut: tooltip onTriggered: switchApplicationPage(flightControlsPage) }, Kirigami.Action { id: navigationMapPageAction checked: currentPage == navigationMapPage iconName: "map-flat" text: i18n("Navigation Map") tooltip: "Alt+3" shortcut: tooltip onTriggered: switchApplicationPage(navigationMapPage) }, Kirigami.Action { id: settingsPageAction checked: currentPage == settingsPage iconName: "configure" text: i18n("Settings") tooltip: "Alt+4" shortcut: tooltip onTriggered: switchApplicationPage(settingsPage) }, Kirigami.Action { id: aboutPageAction checked: currentPage == aboutPage iconName: "help-about" text: i18n("About") tooltip: "Alt+5" shortcut: tooltip onTriggered: switchApplicationPage(aboutPage) } ] } onConnectedChanged: { if (connected && kirogiSettings.allowLocationRequests && !locationPermissions.granted) { locationPermissions.request(); } } onFlyingChanged: { kirogiSettings.flying = flying; kirogiSettings.save(); } function switchApplicationPage(page) { if (!page || currentPage == page) { return; } pageStack.removePage(page); pageStack.push(page); page.forceActiveFocus(); } Kirogi.VehicleSupportPluginModel { id: pluginModel property QtObject currentPlugin: null property string currentPluginName: "" onPluginLoaded: { kirogiSettings.lastPlugin = pluginId; kirogiSettings.save(); currentPluginName = name; currentPlugin = plugin; } Component.onCompleted: { if (kirogiSettings.lastPlugin) { loadPluginById(kirogiSettings.lastPlugin); } } } Connections { target: currentPlugin onVehicleAdded: { currentVehicle = vehicle; } } - PositionSource { - id: gpsPosition - - readonly property real distance: { - if (!valid || !active) { - return 0.0; - } - - if (!_lastKnownCoordinate || !_lastKnownCoordinate.isValid) { - return 0.0; - } - - // If currentVehicle is null or there is no valid position - // set distance to zero. - if (!currentVehicle || !currentVehicle.gpsPosition.isValid) { - return 0.0; - } - - return _lastKnownCoordinate.distanceTo(currentVehicle.gpsPosition); - } - - property var _lastKnownCoordinate: null - - active: kirogiSettings.allowLocationRequests && locationPermissions.granted - updateInterval: 5000 - - preferredPositioningMethods: PositionSource.SatellitePositioningMethods - - onPositionChanged: { - // Position via internet IP does not provide a valid altitude. - if (position.latitudeValid && position.longitudeValid) { - _lastKnownCoordinate = position.coordinate - - if(currentVehicle) { - currentVehicle.setControllerGpsPosition(_lastKnownCoordinate) - } - } - } - } - FontMetrics { id: fontMetrics } Vehicle { id: vehiclePage enabled: currentPage == vehiclePage visible: enabled } FlightControls { id: flightControlsPage enabled: currentPage == flightControlsPage visible: enabled } NavigationMap { id: navigationMapPage enabled: currentPage == navigationMapPage visible: enabled } Settings { id: settingsPage enabled: currentPage == settingsPage visible: enabled } Kirigami.AboutPage { id: aboutPage enabled: currentPage == aboutPage visible: enabled aboutData: kirogiAboutData } Timer { id: resetPersistentFlyingStateTimer interval: 3000 repeat: false onTriggered: { kirogiSettings.flying = flying; kirogiSettings.save(); } } Component.onCompleted: { switchApplicationPage(kirogiSettings.flying ? flightControlsPage : vehiclePage); resetPersistentFlyingStateTimer.start(); + Kirogi.PositionSource.enabled = Qt.binding(function() { return kirogiSettings.allowLocationRequests && locationPermissions.granted }); } }