Revision control
Copy as Markdown
// 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
}
}
}
}
}