Robot Monitor

// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick3D

Rectangle {
    id: root

    radius: Theme.radius
    color: Theme.card
    border.color: Theme.border
    border.width: 1

    required property MainViewModel mainViewModel
    View3D {
        id: topLeftView
        anchors.fill: parent
        anchors.margins: Theme.radius

        camera: OrthographicCamera {
            id: topDownCamera
            z: 600

            property real zoomLevel: 40
            property real yawAngle: 270

            eulerRotation: Qt.vector3d(0, 0, yawAngle)
            horizontalMagnification: zoomLevel
            verticalMagnification: zoomLevel
        }

        environment: SceneEnvironment {
            clearColor: "#181818"
            backgroundMode: SceneEnvironment.Color
            antialiasingMode: SceneEnvironment.MSAA
        }

        Node {

            id: mapFixedFrame

            GridNode {
                z: 0.01
                visible: root.mainViewModel.gridSettings.visible
                opacity: root.mainViewModel.gridSettings.opacity
                resolution: root.mainViewModel.gridSettings.cellCount
                gridStep: root.mainViewModel.gridSettings.cellWidth
            }

            OccupancyGrid {
                id: mapOccGrid
                grid: root.mainViewModel.mapGrid
                scheme: root.mainViewModel.mapSettings.colorScheme
                opacity: root.mainViewModel.mapSettings.opacity
                visible: root.mainViewModel.mapSettings.visible
            }

            OccupancyGrid {
                id: globalCostmapOccGrid

                grid: root.mainViewModel.globalCostmapGrid
                scheme: root.mainViewModel.globalCostmapSettings.colorScheme
                opacity: root.mainViewModel.globalCostmapSettings.opacity
                visible: root.mainViewModel.globalCostmapSettings.visible
            }

            ShapeNode {
                id: plan
                shapeData: root.mainViewModel.plan
            }

            Node {

                id: odometryFrame
                position: root.mainViewModel.map_odom_p
                rotation: root.mainViewModel.map_odom_q

                OccupancyGrid {
                    id: localCostmapOccGrid

                    grid: root.mainViewModel.localCostmapGrid
                    scheme: root.mainViewModel.localCostmapSettings.colorScheme
                    opacity: root.mainViewModel.localCostmapSettings.opacity
                    visible: root.mainViewModel.localCostmapSettings.visible
                }

                Node {
                    id: baseLinkFrame
                    position: root.mainViewModel.odom_base_link_p
                    rotation: root.mainViewModel.odom_base_link_q

                    Node {
                        id: shellLinkFrame
                        position: root.mainViewModel.base_link_shell_link_p
                        rotation: root.mainViewModel.base_link_shell_link_q

                        Node {
                            id: rpLidarLinkFrame
                            position: root.mainViewModel.shell_link_rplidar_link_p
                            rotation: root.mainViewModel.shell_link_rplidar_link_q
                            Node {
                                id: turtlebot4RPLidarLinkRPLidarFrame
                                position: root.mainViewModel.rplidar_link_turtlebot4_rplidar_link_rplidar_p
                                rotation: root.mainViewModel.rplidar_link_turtlebot4_rplidar_link_rplidar_q

                                LaserScan {
                                    id: laserScan
                                    instancing: root.mainViewModel.laserScanInstanceList
                                    visible: root.mainViewModel.laserScanSettings.visible
                                    opacity: root.mainViewModel.laserScanSettings.opacity
                                    color: root.mainViewModel.laserScanSettings.color
                                    pointSize: root.mainViewModel.laserScanSettings.pointSize
                                }
                            }
                        }
                    }
                }

                ShapeNode {
                    id: footprint
                    shapeData: root.mainViewModel.footprint
                }
            }
        }

        Item {
            id: arrowContainer
            width: 30
            height: 30
            visible: false
            rotation: topDownCamera.yawAngle + 90

            Rectangle {
                anchors.centerIn: parent

                width: 10
                height: 10
                radius: 5
                border.width: 2
                border.color: "#1A2332"
                color: "#2BA3B4"
            }

            Image {
                id: arrowImg
                anchors.horizontalCenter: parent.horizontalCenter
                anchors.bottom: parent.verticalCenter
                transformOrigin: Image.Bottom
                width: 40
                height: 60
                source: "images/arrow.svg"
                rotation: 0
            }
        }

        Rectangle {
            anchors.left: parent.left
            anchors.right: parent.right
            anchors.bottom: parent.bottom
            height: 32
            anchors.margins: 20
            radius: Theme.radius
            color: Theme.card
            border.color: Theme.border
            border.width: 1
            opacity: 0.8
            visible: mouseArea.containsMouse && !mouseArea.isDragging
                     && !arrowContainer.visible

            Text {
                anchors.centerIn: parent
                text: "Left Click + Drag: Pan    |    Ctrl + Left Click: Navigate    |    Ctrl + Right Click: Center    |    Wheel: Zoom    |    Ctrl + Wheel: Rotate"
                color: Theme.foreground
                font.pixelSize: 14
                font.bold: true
            }
        }

        MouseArea {
            id: mouseArea
            anchors.fill: parent
            hoverEnabled: true
            acceptedButtons: Qt.LeftButton | Qt.RightButton
            property bool isDragging: false
            property point lastMousePos: Qt.point(0, 0)

            onPressed: mouse => {
                           if (mouse.button === Qt.LeftButton)
                           if (mouse.modifiers & Qt.ControlModifier) {
                               arrowImg.rotation = 0
                               arrowContainer.x = mouse.x - arrowContainer.width / 2
                               arrowContainer.y = mouse.y - arrowContainer.height / 2
                               arrowContainer.visible = true
                           } else {
                               mouseArea.isDragging = true
                               lastMousePos = Qt.point(mouse.x, mouse.y)
                           }
                       }

            onPositionChanged: mouse => {
                                   if (arrowContainer.visible) {

                                       const cx = arrowContainer.x + arrowContainer.width / 2
                                       const cy = arrowContainer.y + arrowContainer.height / 2
                                       const dx = mouse.x - cx
                                       const dy = mouse.y - cy

                                       let angleDeg = Math.atan2(
                                           dy, dx) * 180 / Math.PI
                                       if (angleDeg < 0)
                                       angleDeg += 360

                                       arrowImg.rotation = angleDeg
                                   } else if (mouseArea.isDragging) {
                                       const deltaX = mouse.x - lastMousePos.x
                                       const deltaY = mouse.y - lastMousePos.y

                                       topDownCamera.position = topDownCamera.position.plus(
                                           Qt.vector3d(deltaY, deltaX, 0).times(
                                               1 / topDownCamera.zoomLevel))

                                       lastMousePos = Qt.point(mouse.x, mouse.y)
                                   }
                               }

            onReleased: mouse => {
                            if (mouse.button === Qt.LeftButton
                                && arrowContainer.visible) {
                                let scenePos = topLeftView.mapTo3DScene(
                                    Qt.vector3d(
                                        arrowContainer.x + arrowContainer.width / 2,
                                        arrowContainer.y + arrowContainer.height / 2,
                                        0))
                                let angleDeg = 360 - arrowImg.rotation
                                root.mainViewModel.navigateToPose(scenePos,
                                                                  angleDeg)
                                arrowContainer.visible = false
                            } else if (mouse.button === Qt.RightButton) {
                                let scenePos = topLeftView.mapTo3DScene(
                                    Qt.vector3d(mouse.x, mouse.y, 0))
                                topDownCamera.x = scenePos.x
                                topDownCamera.y = scenePos.y
                            }
                            mouseArea.isDragging = false
                        }

            onWheel: wheel => {
                         let modifier = wheel.angleDelta.y > 0 ? 1 : -1
                         if (wheel.modifiers & Qt.ControlModifier) {
                             topDownCamera.yawAngle += (modifier * 2)
                         } else {
                             let zoomLevel = Math.max(
                                 10, topDownCamera.zoomLevel + modifier * 5)
                             topDownCamera.zoomLevel = zoomLevel
                         }
                     }
        }
    }
}