// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause pragma ComponentBehavior: Bound import QtQuick import QtQuick.Shapes import SatelliteInformation Rectangle { id: root // Multisample this item to get rid of aliasing in our custom shapes. layer.enabled: true layer.samples: 4 // Sample text to calculate sky view size properly Text { id: sampleText visible: false text: "270°" font.pixelSize: Theme.smallFontSize font.weight: Theme.fontLightWeight } required property SatelliteModel satellitesModel required property color inViewColor required property color inUseColor readonly property real fullRadius: width < height ? width / 2 - sampleText.width * 1.1 : height / 2 - sampleText.height * 1.1 readonly property real maxUsedRadius: 0.9 * fullRadius readonly property real radiusStep: maxUsedRadius / 9 readonly property int centerX: width / 2 readonly property int centerY: height / 2 color: Theme.darkBackgroundColor LegendBox { id: legend anchors { top: root.top right: root.right margins: Theme.defaultSpacing } } // Shape for the SkyView Shape { id: rootShape readonly property color mainColor: Theme.textMainColor readonly property color dashedColor: Qt.rgba(mainColor.r, mainColor.g, mainColor.b, 0.5) // Circles component ElevationCircle: ShapePath { id: circle property color color: "white" property real radius: 0 property bool dashed: false strokeColor: circle.color fillColor: "transparent" strokeStyle: circle.dashed ? ShapePath.DashLine : ShapePath.SolidLine dashPattern: [4, 4] PathAngleArc { centerX: root.centerX centerY: root.centerY radiusX: circle.radius radiusY: circle.radius startAngle: 0 sweepAngle: 360 } } ElevationCircle { radius: root.fullRadius color: Theme.greenColor } ElevationCircle { radius: root.radiusStep * 9 color: rootShape.dashedColor dashed: true } ElevationCircle { radius: root.radiusStep * 7 color: rootShape.dashedColor dashed: true } ElevationCircle { radius: root.radiusStep * 5 color: rootShape.dashedColor dashed: true } ElevationCircle { radius: root.radiusStep * 3 color: rootShape.dashedColor dashed: true } ElevationCircle { radius: root.radiusStep color: rootShape.mainColor } // Lines component Line: ShapePath { id: line readonly property color green: Theme.greenColor strokeColor: Qt.rgba(green.r, green.g, green.b, 0.5) property real angle property real radianAngle: line.angle / 180 * Math.PI startX: root.centerX + root.fullRadius * Math.cos(radianAngle) startY: root.centerY + root.fullRadius * Math.sin(radianAngle) PathLine { x: root.centerX + root.fullRadius * Math.cos(line.radianAngle + Math.PI) y: root.centerY + root.fullRadius * Math.sin(line.radianAngle + Math.PI) } } Line { angle: 0 } Line { angle: 30 } Line { angle: 60 } Line { angle: 90 } Line { angle: 120 } Line { angle: 150 } // Azimuth captions component AzimuthCaption: Text { id: textElement property real angle property real offsetX: 0 property real offsetY: 0 // Subtract 90, because 0 should be on top, not on the right property real radianAngle: (angle - 90) / 180 * Math.PI // end of line coordinates property int lineX: root.centerX + root.fullRadius * Math.cos(radianAngle) property int lineY: root.centerY + root.fullRadius * Math.sin(radianAngle) x: lineX + offsetX y: lineY + offsetY text: angle.toFixed(0) + '°' color: Theme.textMainColor font.pixelSize: Theme.smallFontSize font.weight: Theme.fontLightWeight } AzimuthCaption { text: 'N' angle: 0 offsetY: -height offsetX: -width / 2 } AzimuthCaption { angle: 30 offsetY: -height } AzimuthCaption { angle: 60 offsetX: width * 0.5 offsetY: -height / 2 } AzimuthCaption { text: 'E' angle: 90 offsetX: width * 0.5 offsetY: -height / 2 } AzimuthCaption { angle: 120 } AzimuthCaption { angle: 150 } AzimuthCaption { text: 'S' angle: 180 offsetX: -width / 2 } AzimuthCaption { angle: 210 offsetX: -width / 2 offsetY: height / 2 } AzimuthCaption { angle: 240 offsetX: -width } AzimuthCaption { text: 'W' angle: 270 offsetX: -width * 1.5 offsetY: -height / 2 } AzimuthCaption { angle: 300 offsetX: -width offsetY: -height } AzimuthCaption { angle: 330 offsetX: -width offsetY: -height } // Elevation captions component ElevationCaption: Text { property real step x: root.centerX - implicitWidth / 2 y: root.centerY - (step * root.radiusStep + implicitHeight) color: Theme.textMainColor font.pixelSize: Theme.smallFontSize font.weight: Theme.fontLightWeight } ElevationCaption { text: "80°" step: 1 } ElevationCaption { text: "60°" step: 3 } ElevationCaption { text: "40°" step: 5 } ElevationCaption { text: "20°" step: 7 } ElevationCaption { text: "0°" step: 9 } } // Actual satellite positions Item { id: satelliteItemView anchors.fill: parent property string selectedName: "" property var selectedData: ({"name": "", "azimuth": 0, "elevation": 0}) // selected{X,Y} holds the center of the selected circle property int selectedX: -1 property int selectedY: -1 Repeater { model: root.satellitesModel delegate: Rectangle { id: satItem required property var modelData property bool selected: satelliteItemView.selectedName === modelData.name readonly property color normalColor: modelData.inUse ? root.inUseColor : root.inViewColor readonly property color selectedColor: Qt.rgba(normalColor.r, normalColor.g, normalColor.b, 0.5) readonly property color centerColor: selected ? Theme.whiteColor : Theme.blackColor readonly property int normalWidth: 10 readonly property int centerWidth: 6 readonly property int outerWidth: selected ? normalWidth + 10 : normalWidth width: outerWidth height: width radius: width / 2 color: selectedColor visible: modelData.azimuth > -1 && modelData.elevation > -1 property real angle: (modelData.azimuth - 90) / 180 * Math.PI property real distance: root.maxUsedRadius * (90 - modelData.elevation) / 90 x: root.centerX + distance * Math.cos(angle) - width / 2 y: root.centerY + distance * Math.sin(angle) - height / 2 Rectangle { anchors.centerIn: parent width: satItem.normalWidth height: width radius: width / 2 color: satItem.normalColor Rectangle { anchors.centerIn: parent width: satItem.centerWidth height: width radius: width / 2 color: satItem.centerColor } } MouseArea { anchors { fill: parent margins: -20 // so that it's easier to select the item } onClicked: { satelliteItemView.selectedName = satItem.modelData.name satelliteItemView.selectedData = satItem.modelData satelliteItemView.selectedX = satItem.x + satItem.width / 2 satelliteItemView.selectedY = satItem.y + satItem.height / 2 } } onXChanged: { if (satItem.selected) satelliteItemView.selectedX = satItem.x + satItem.width / 2 } onYChanged: { if (satItem.selected) satelliteItemView.selectedY = satItem.y + satItem.height / 2 } Component.onCompleted: { if (satItem.selected) { satelliteItemView.selectedData = satItem.modelData satelliteItemView.selectedX = satItem.x + satItem.width / 2 satelliteItemView.selectedY = satItem.y + satItem.height / 2 } } } } Rectangle { id: blurRect anchors.fill: parent color: Theme.backgroundBlurColor visible: satelliteItemView.selectedName !== "" MouseArea { anchors.fill: parent onClicked: satelliteItemView.selectedName = "" } } Rectangle { id: satInfoRect visible: satelliteItemView.selectedName !== "" implicitWidth: Math.max(satName.width, satAzimuth.width, satElevation.width) + satInfoCol.anchors.leftMargin + satInfoCol.anchors.rightMargin implicitHeight: satName.height + satAzimuth.height + satElevation.height + satInfoCol.anchors.topMargin + satInfoCol.anchors.bottomMargin + 2 * satInfoCol.spacing // We need to make sure that the popup fits into the view. // This depends on the position of the actual satellite. // We try to show the popup top-right from the satellite, // but adjust if it's too close to the window border. // Keep in mind that (0, 0) is the top left corner. readonly property int xOffset: 15 readonly property int yOffset: 25 x: satelliteItemView.selectedX + satInfoRect.xOffset + satInfoRect.width < satelliteItemView.width ? satelliteItemView.selectedX + satInfoRect.xOffset : satelliteItemView.selectedX - satInfoRect.xOffset - satInfoRect.width y: satelliteItemView.selectedY - satInfoRect.yOffset - satInfoRect.height > 0 ? satelliteItemView.selectedY - satInfoRect.yOffset - satInfoRect.height : satelliteItemView.selectedY + satInfoRect.yOffset color: Theme.whiteColor border { color: Theme.lightGrayColor width: 1 } radius: 8 MouseArea { anchors.fill: parent onClicked: {} // suppress mouse area of blurRect } Column { id: satInfoCol anchors { fill: parent topMargin: Theme.defaultSpacing bottomMargin: Theme.defaultSpacing leftMargin: Theme.defaultSpacing * 2 rightMargin: Theme.defaultSpacing * 2 } spacing: 0 Text { id: satName text: qsTr("Sat ") + satelliteItemView.selectedData.name font.pixelSize: Theme.smallFontSize font.weight: Theme.fontDefaultWeight color: Theme.darkGrayColor } Text { id: satAzimuth text: qsTr("Azimuth: ") + satelliteItemView.selectedData.azimuth.toFixed(0) + "°" font.pixelSize: Theme.smallFontSize font.weight: Theme.fontLightWeight color: Theme.grayColor } Text { id: satElevation text: qsTr("Elevation: ") + satelliteItemView.selectedData.elevation.toFixed(0) + "°" font.pixelSize: Theme.smallFontSize font.weight: Theme.fontLightWeight color: Theme.grayColor } } } Shape { id: connectionLine visible: satelliteItemView.selectedName !== "" ShapePath { strokeColor: Theme.whiteColor strokeWidth: 1 startX: satelliteItemView.selectedX startY: satelliteItemView.selectedY PathLine { x: satInfoRect.x > satelliteItemView.selectedX ? satInfoRect.x : satInfoRect.x + satInfoRect.width y: satInfoRect.y > satelliteItemView.selectedY ? satInfoRect.y : satInfoRect.y + satInfoRect.height } } } } }