From 694ac0ceb5ad65f40ad26887d17235361aedf517 Mon Sep 17 00:00:00 2001 From: Val Doroshchuk Date: Wed, 23 Jun 2021 17:17:16 +0200 Subject: [PATCH] Introduce obstacle distance overlay It receives mavlink message obstacle_distance and shows distances in a circular array fashion. Added a setting in Safety/Object Detection section to show the overlay. Can be used independently to collision prevention features. There are two overlays: Video and Map overlay In different styles. The styles could be changed in particular qml file. https://github.com/mavlink/qgroundcontrol/issues/7670 --- qgroundcontrol.qrc | 3 + src/AutoPilotPlugins/PX4/SafetyComponent.qml | 10 ++ src/FlightDisplay/CMakeLists.txt | 3 + src/FlightDisplay/FlyViewMap.qml | 5 + src/FlightDisplay/FlyViewVideo.qml | 5 + src/FlightDisplay/ObstacleDistanceOverlay.qml | 71 +++++++++++++ src/FlightDisplay/ObstacleDistanceOverlayMap.qml | 114 +++++++++++++++++++++ src/FlightDisplay/ObstacleDistanceOverlayVideo.qml | 113 ++++++++++++++++++++ .../QGroundControl/FlightDisplay/qmldir | 3 + src/Settings/FlyView.SettingsGroup.json | 6 ++ src/Settings/FlyViewSettings.cc | 1 + src/Settings/FlyViewSettings.h | 1 + 12 files changed, 335 insertions(+) create mode 100644 src/FlightDisplay/ObstacleDistanceOverlay.qml create mode 100644 src/FlightDisplay/ObstacleDistanceOverlayMap.qml create mode 100644 src/FlightDisplay/ObstacleDistanceOverlayVideo.qml diff --git a/qgroundcontrol.qrc b/qgroundcontrol.qrc index b445dae..677bda3 100644 --- a/qgroundcontrol.qrc +++ b/qgroundcontrol.qrc @@ -233,6 +233,9 @@ src/FlightDisplay/TerrainProgress.qml src/FlightDisplay/TelemetryValuesBar.qml src/FlightDisplay/VehicleWarnings.qml + src/FlightDisplay/ObstacleDistanceOverlay.qml + src/FlightDisplay/ObstacleDistanceOverlayMap.qml + src/FlightDisplay/ObstacleDistanceOverlayVideo.qml src/QmlControls/QGroundControl/FlightDisplay/qmldir src/FlightMap/MapItems/CameraTriggerIndicator.qml src/FlightMap/Widgets/CenterMapDropButton.qml diff --git a/src/AutoPilotPlugins/PX4/SafetyComponent.qml b/src/AutoPilotPlugins/PX4/SafetyComponent.qml index cc02769..7e04b57 100644 --- a/src/AutoPilotPlugins/PX4/SafetyComponent.qml +++ b/src/AutoPilotPlugins/PX4/SafetyComponent.qml @@ -176,6 +176,7 @@ SetupPage { if(_collisionPrevention) { _collisionPrevention.value = index > 0 ? 5 : -1 console.log('Collision prevention enabled: ' + _collisionPrevention.value) + showObstacleDistanceOverlayCheckBox.checked = _collisionPrevention.value > 0 } } } @@ -231,6 +232,15 @@ SetupPage { } } } + + FactCheckBox { + id: showObstacleDistanceOverlayCheckBox + text: qsTr("Show obstacle distance overlay") + visible: _showObstacleDistanceOverlay.visible + fact: _showObstacleDistanceOverlay + + property Fact _showObstacleDistanceOverlay: QGroundControl.settingsManager.flyViewSettings.showObstacleDistanceOverlay + } } } } diff --git a/src/FlightDisplay/CMakeLists.txt b/src/FlightDisplay/CMakeLists.txt index 29c5ce3..6aafa3c 100644 --- a/src/FlightDisplay/CMakeLists.txt +++ b/src/FlightDisplay/CMakeLists.txt @@ -46,5 +46,8 @@ add_custom_target(FligthDisplayQml VehicleWarnings.qml VirtualJoystick.qml VTOLChecklist.qml + ObstacleDistanceOverlay.qml + ObstacleDistanceOverlayMap.qml + ObstacleDistanceOverlayVideo.qml ) diff --git a/src/FlightDisplay/FlyViewMap.qml b/src/FlightDisplay/FlyViewMap.qml index 202824f..b00e283 100644 --- a/src/FlightDisplay/FlyViewMap.qml +++ b/src/FlightDisplay/FlyViewMap.qml @@ -223,6 +223,11 @@ FlightMap { planMasterController: _planMasterController } + ObstacleDistanceOverlayMap { + id: obstacleDistance + showText: !pipMode + } + // Add trajectory lines to the map MapPolyline { id: trajectoryPolyline diff --git a/src/FlightDisplay/FlyViewVideo.qml b/src/FlightDisplay/FlyViewVideo.qml index 551ba39..16f3821 100644 --- a/src/FlightDisplay/FlyViewVideo.qml +++ b/src/FlightDisplay/FlyViewVideo.qml @@ -83,4 +83,9 @@ Item { anchors.fill: parent vehicle: QGroundControl.multiVehicleManager.activeVehicle } + + ObstacleDistanceOverlayVideo { + id: obstacleDistance + showText: pipState.state === pipState.fullState + } } diff --git a/src/FlightDisplay/ObstacleDistanceOverlay.qml b/src/FlightDisplay/ObstacleDistanceOverlay.qml new file mode 100644 index 0000000..64dda7a --- /dev/null +++ b/src/FlightDisplay/ObstacleDistanceOverlay.qml @@ -0,0 +1,71 @@ +/**************************************************************************** + * + * (c) 2009-2020 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +import QtQuick 2.11 + +import QGroundControl 1.0 +import QGroundControl.SettingsManager 1.0 + +Canvas { + id: canvas + anchors.fill: parent + visible: QGroundControl.settingsManager.flyViewSettings.showObstacleDistanceOverlay.value > 0 && _activeVehicle && _activeVehicle.objectAvoidance.available + + property var showText: true + property var interval: 200 + property var _ranges: [] + property var _incrementDeg: 0 + property var _offsetDeg: 0 + property var _heading: 0 + property var _maxRadiusMeters: 0 + property var _rangesLen: 0 + property var _degToRangeIdx: function(deg, useHeading) { + return rangeIdx(deg, _incrementDeg, _offsetDeg, _rangesLen, useHeading ? _heading : 0) + } + property var _rangeToShow: function(range) { + const feets = QGroundControl.settingsManager.unitsSettings.horizontalDistanceUnits.value === UnitsSettings.HorizontalDistanceUnitsFeet + range = feets ? range * 3.2808399 : range + return range.toFixed(2) + } + + // Converts degrees to index from ranges + function rangeIdx(deg, increment, offset, len, heading) { + const i = (360 - heading + deg - offset) / increment + return (len + Math.ceil(i)) % len + } + + Timer { + interval: canvas.interval + running: true + repeat: true + onTriggered: canvas.requestPaint() + } + + onPaint: { + if (!_activeVehicle) + return + + var ctx = getContext("2d"); + ctx.reset() + + _ranges = _activeVehicle.objectAvoidance.distances + _incrementDeg = _activeVehicle.objectAvoidance.increment + if (_ranges.length == 0 || _incrementDeg == 0) + return + + _offsetDeg = _activeVehicle.objectAvoidance.angleOffset + _heading = _activeVehicle.heading.value + _maxRadiusMeters = _activeVehicle.objectAvoidance.maxDistance / 100 + _rangesLen = 360.0 / _incrementDeg + if (_rangesLen > _ranges.length) + _rangesLen = _ranges.length + + paintObstacleOverlay(ctx) + } +} diff --git a/src/FlightDisplay/ObstacleDistanceOverlayMap.qml b/src/FlightDisplay/ObstacleDistanceOverlayMap.qml new file mode 100644 index 0000000..b9a16fc --- /dev/null +++ b/src/FlightDisplay/ObstacleDistanceOverlayMap.qml @@ -0,0 +1,114 @@ +/**************************************************************************** + * + * (c) 2009-2020 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +import QtQuick 2.11 + +import QGroundControl 1.0 +import QGroundControl.SettingsManager 1.0 + +Item { + id: root + anchors.fill: parent + property var showText: obstacleDistance._showText + + function paintObstacleOverlay(ctx) { + const vehiclePoint = _root.fromCoordinate(_activeVehicleCoordinate, false) + const centerX = vehiclePoint.x + const centerY = vehiclePoint.y + const maxRadiusPixels = 0.9 * root.height / 2 // Max pixels to center + const minRadiusPixels = maxRadiusPixels * 0.2 + const metersPerPixelInCycle = (maxRadiusPixels - minRadiusPixels) / obstacleDistance._maxRadiusMeters + + const leftCoord = mapControl.toCoordinate(Qt.point(0, root.y), false) + const rightCoord = mapControl.toCoordinate(Qt.point(100, root.y), false) + const metersIn100Pixels = leftCoord.distanceTo(rightCoord) + const metersPerPixel = 100.0 / metersIn100Pixels + + var minGradPixels = minRadiusPixels + var maxGradPixels = maxRadiusPixels + var metersToPixels = metersPerPixelInCycle + if (metersIn100Pixels < 4) { + minGradPixels = 0 + maxGradPixels = obstacleDistance._maxRadiusMeters * metersPerPixel + metersToPixels = metersPerPixel + } + + var grad = ctx.createRadialGradient(centerX, centerY, minGradPixels, centerX, centerY, maxGradPixels) + grad.addColorStop(0, Qt.rgba(1, 0, 0, 1)) + grad.addColorStop(0.1, Qt.rgba(1, 0, 0, 0.7)) + grad.addColorStop(0.5, Qt.rgba(1, 0.64, 0, 0.7)) + grad.addColorStop(0.65, Qt.rgba(1, 0.64, 0, 0.3)) + grad.addColorStop(0.95, Qt.rgba(0, 1, 0, 0.3)) + grad.addColorStop(1, Qt.rgba(0, 1, 0, 0)) + + var points = [] + const height = minRadiusPixels / 8 + for (var i = 0; i < obstacleDistance._rangesLen; ++i) { + const deg = i * obstacleDistance._incrementDeg + const rad = deg * Math.PI / 180.0 + const m = obstacleDistance._ranges[obstacleDistance._degToRangeIdx(deg, true)] / 100.0 + const pixels = minGradPixels + m * metersToPixels + const outerX = centerX + pixels * Math.cos(rad) + const outerY = centerY + pixels * Math.sin(rad) + const innerX = centerX + (pixels - height) * Math.cos(rad) + const innerY = centerY + (pixels - height) * Math.sin(rad) + + points.push({'outer_x': outerX, 'outer_y': outerY, 'inner_x': innerX, 'inner_y': innerY, 'range': m}) + } + + ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.8) + ctx.font = "bold 22px sans-serif" + ctx.lineWidth = 2; + var mPrev = -1 + for (var i = 0; i < points.length; i += 3) { + const i3 = (i + 3) % points.length // catch the line from the last to the first point + + ctx.beginPath() + ctx.fillStyle = grad + ctx.moveTo(points[i].inner_x, points[i].inner_y) + ctx.lineTo(points[i].outer_x, points[i].outer_y) + ctx.bezierCurveTo( + points[i + 1].outer_x, points[i + 1].outer_y, + points[i + 2].outer_x, points[i + 2].outer_y, + points[i3].outer_x, points[i3].outer_y) + ctx.lineTo(points[i3].inner_x, points[i3].inner_y) + ctx.bezierCurveTo( + points[i3].inner_x, points[i3].inner_y, + points[i + 2].inner_x, points[i + 2].inner_y, + points[i + 1].inner_x, points[i + 1].inner_y) + ctx.fill() + + if (showText) { + var iMin = i + for (var k = iMin + 1; k < iMin + 2; ++k) { + const idx = k % points.length + if (points[idx].range < points[iMin].range) + iMin = idx + } + + var m = points[iMin].range + if (m < obstacleDistance._maxRadiusMeters && Math.abs(m - mPrev) > 2.0) { + const textX = points[iMin].inner_x + const textY = points[iMin].inner_y + + ctx.fillStyle = Qt.rgba(1, 1, 1, 0.9) + const text = obstacleDistance._rangeToShow(m) + ctx.strokeText(text, textX, textY) + ctx.fillText(text, textX, textY) + mPrev = m + } + } + } + } + + ObstacleDistanceOverlay { + id: obstacleDistance + } +} + diff --git a/src/FlightDisplay/ObstacleDistanceOverlayVideo.qml b/src/FlightDisplay/ObstacleDistanceOverlayVideo.qml new file mode 100644 index 0000000..2d51c1e --- /dev/null +++ b/src/FlightDisplay/ObstacleDistanceOverlayVideo.qml @@ -0,0 +1,113 @@ +/**************************************************************************** + * + * (c) 2009-2020 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +import QtQuick 2.11 + +import QGroundControl 1.0 +import QGroundControl.SettingsManager 1.0 + +Item { + id: root + anchors.fill: parent + property var showText: obstacleDistance._showText + + function drawSegment(ctx, range, centerX, centerY, lengthFrom, lengthTo, radFrom, radTo, grad) { + const topSrcX = centerX + lengthFrom * Math.cos(radFrom) + const topSrcY = centerY + lengthFrom * Math.sin(radFrom) + const topDstX = centerX + lengthFrom * Math.cos(radTo) + const topDstY = centerY + lengthFrom * Math.sin(radTo) + + ctx.save() + ctx.setTransform(2, 0, 0, 1, -centerX, 0) + ctx.beginPath() + ctx.fillStyle = grad + ctx.moveTo(topSrcX, topSrcY) + ctx.arc(centerX, centerY, lengthFrom, radFrom, radTo, false) + ctx.arc(centerX, centerY, lengthTo, radTo, radFrom, true) + ctx.lineTo(topSrcX, topSrcY) + ctx.fill() + ctx.closePath() + ctx.restore() + + if (range > 0) { + ctx.save() + ctx.setTransform(2, 0, 0, 2, -centerX, 0) + ctx.beginPath() + ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.8) + ctx.font = "bold 10px sans-serif" + ctx.lineWidth = 2 + ctx.fillStyle = Qt.rgba(1, 1, 1, 0.8) + const text = obstacleDistance._rangeToShow(range) + const textX = topSrcX + (topDstX - topSrcX) / 2 + const textY = topSrcY + (topDstY - topSrcY) / 2 + ctx.strokeText(text, textX, textY / 2) + ctx.fillText(text, textX, textY / 2) + ctx.closePath() + ctx.restore() + } + } + + function paintObstacleOverlay(ctx) { + const centerX = root.width / 2 + const centerY = root.height / 2 + const maxRadiusPixels = 0.9 * root.height / 2 // Max pixels to center + const minRadiusPixels = maxRadiusPixels * 0.2 // Min radius in pixels to center + const minGradPixels = minRadiusPixels + const maxGradPixels = maxRadiusPixels + const segmentHeightPixels = minRadiusPixels / 8 + const levelMeters = 10 + const levelNum = obstacleDistance._maxRadiusMeters / levelMeters + + var grad = ctx.createRadialGradient(centerX, centerY, maxGradPixels - segmentHeightPixels * levelNum * 2, centerX, centerY, maxGradPixels) + grad.addColorStop(0, Qt.rgba(1, 0, 0, 0.9)) + grad.addColorStop(0.1, Qt.rgba(1, 0, 0, 0.3)) + grad.addColorStop(0.5, Qt.rgba(1, 0.64, 0, 0.3)) + grad.addColorStop(0.65, Qt.rgba(1, 0.64, 0, 0.2)) + grad.addColorStop(0.95, Qt.rgba(0, 1, 0, 0.1)) + grad.addColorStop(1, Qt.rgba(0, 1, 0, 0)) + + const segNum = 16 + const incDeg = 360 / segNum + for (var s = 0; s < segNum; ++s) { + const deg = s * 360.0 / segNum + const rad = deg * Math.PI / 180.0 + const i = obstacleDistance._degToRangeIdx(deg, false) + const iNext = obstacleDistance._degToRangeIdx(deg + incDeg, false) + var rangeMin = obstacleDistance._maxRadiusMeters + + const end = i < iNext ? iNext : obstacleDistance._rangesLen + iNext + for (var ii = i; ii < end; ++ii) { + const r = obstacleDistance._ranges[ii % obstacleDistance._rangesLen] / 100 + if (r < rangeMin) + rangeMin = r + } + + const lengthFrom = maxRadiusPixels + const radFrom = rad + const radTo = radFrom + incDeg * Math.PI / 180.0 - 0.03 + var range = obstacleDistance._maxRadiusMeters + for (var ii = 0; ii < levelNum; ++ii) { + const from = lengthFrom - ii * segmentHeightPixels * 2 + const to = from - segmentHeightPixels + const rangeInLevel = obstacleDistance._maxRadiusMeters - (ii + 1) * levelMeters + const isLast = ii >= levelNum - 1 + if (rangeMin > rangeInLevel || isLast) + range = rangeMin + const rangeToShow = showText && range < obstacleDistance._maxRadiusMeters ? range : 0 + drawSegment(ctx, rangeToShow, centerX, centerY, from, to, radFrom, radTo, grad) + if (range == rangeMin) + break + } + } + } + + ObstacleDistanceOverlay { + id: obstacleDistance + } +} diff --git a/src/QmlControls/QGroundControl/FlightDisplay/qmldir b/src/QmlControls/QGroundControl/FlightDisplay/qmldir index 727ac69..f44dd8c 100644 --- a/src/QmlControls/QGroundControl/FlightDisplay/qmldir +++ b/src/QmlControls/QGroundControl/FlightDisplay/qmldir @@ -33,3 +33,6 @@ ProximityRadarVideoView 1.0 ProximityRadarVideoView.qml TerrainProgress 1.0 TerrainProgress.qml TelemetryValuesBar 1.0 TelemetryValuesBar.qml VehicleWarnings 1.0 VehicleWarnings.qml +ObstacleDistanceOverlay 1.0 ObstacleDistanceOverlay.qml +ObstacleDistanceOverlayMap 1.0 ObstacleDistanceOverlayMap.qml +ObstacleDistanceOverlayVideo 1.0 ObstacleDistanceOverlayVideo.qml diff --git a/src/Settings/FlyView.SettingsGroup.json b/src/Settings/FlyView.SettingsGroup.json index 582bbb1..c5bbeb5 100644 --- a/src/Settings/FlyView.SettingsGroup.json +++ b/src/Settings/FlyView.SettingsGroup.json @@ -54,6 +54,12 @@ "default": true }, { + "name": "showObstacleDistanceOverlay", + "shortDesc": "Show obstacle distance overlay on map and video.", + "type": "bool", + "default": false +}, +{ "name": "maxGoToLocationDistance", "shortDesc": "Maximum distance allowed for Go To Location.", "type": "double", diff --git a/src/Settings/FlyViewSettings.cc b/src/Settings/FlyViewSettings.cc index e99fae5..5b8fb41 100644 --- a/src/Settings/FlyViewSettings.cc +++ b/src/Settings/FlyViewSettings.cc @@ -26,3 +26,4 @@ DECLARE_SETTINGSFACT(FlyViewSettings, lockNoseUpCompass) DECLARE_SETTINGSFACT(FlyViewSettings, maxGoToLocationDistance) DECLARE_SETTINGSFACT(FlyViewSettings, keepMapCenteredOnVehicle) DECLARE_SETTINGSFACT(FlyViewSettings, showSimpleCameraControl) +DECLARE_SETTINGSFACT(FlyViewSettings, showObstacleDistanceOverlay) diff --git a/src/Settings/FlyViewSettings.h b/src/Settings/FlyViewSettings.h index 71b08df..237ad32 100644 --- a/src/Settings/FlyViewSettings.h +++ b/src/Settings/FlyViewSettings.h @@ -28,4 +28,5 @@ public: DEFINE_SETTINGFACT(maxGoToLocationDistance) DEFINE_SETTINGFACT(keepMapCenteredOnVehicle) DEFINE_SETTINGFACT(showSimpleCameraControl) + DEFINE_SETTINGFACT(showObstacleDistanceOverlay) };