dcs_liberation/qt_ui/widgets/QConditionsWidget.py
Dan Albert 656a98675e Rework sim status update to not need a thread.
Rather than polling at 60Hz (which may be faster than the tick rate,
wasting cycles; and also makes synchronization annoying), collect events
during the tick and emit them after (rate limited, pooling events until
it is time for another event to send).

This can be improved by paying attention to the aircraft update list,
which would allow us to avoid updating aircraft that don't have a status
change. To do that we need to be able to quickly lookup a FlightJs
matching a Flight through, and Flight isn't hashable.

We should also be removing dead events and de-duplicating. Currently
each flight has an update for every tick, but only the latest one
matters. Combat update events also don't matter if the same combat is
new in the update.

https://github.com/dcs-liberation/dcs_liberation/issues/1680
2021-12-23 17:46:24 -08:00

316 lines
11 KiB
Python

from datetime import datetime
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import (
QFrame,
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QVBoxLayout,
)
from dcs.weather import CloudPreset, Weather as PydcsWeather
import qt_ui.uiconstants as CONST
from game.sim.gameupdateevents import GameUpdateEvents
from game.utils import mps
from game.weather import Conditions, TimeOfDay
from qt_ui.simcontroller import SimController
class QTimeTurnWidget(QGroupBox):
"""
UI Component to display current turn and time info
"""
def __init__(self, sim_controller: SimController) -> None:
super(QTimeTurnWidget, self).__init__("Turn")
self.sim_controller = sim_controller
self.setStyleSheet(
"padding: 0px; margin-left: 5px; margin-right: 0px; margin-top: 1ex; margin-bottom: 5px; border-right: 0px"
)
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
# self.setProperty('style', 'conditions__widget--turn')
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.daytime_icon = QLabel()
self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.daytime_icon)
self.time_column = QVBoxLayout()
self.layout.addLayout(self.time_column)
self.date_display = QLabel()
self.time_column.addWidget(self.date_display)
self.time_display = QLabel()
self.time_column.addWidget(self.time_display)
sim_controller.sim_update.connect(self.on_sim_update)
def on_sim_update(self, _events: GameUpdateEvents) -> None:
time = self.sim_controller.current_time_in_sim
if time is None:
self.date_display.setText("")
self.time_display.setText("")
else:
self.set_date_and_time(time)
def set_current_turn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.daytime_icon.setPixmap(self.icons[conditions.time_of_day])
self.set_date_and_time(conditions.start_time)
self.setTitle(f"Turn {turn}")
def set_date_and_time(self, time: datetime) -> None:
self.date_display.setText(time.strftime("%d %b %Y"))
self.time_display.setText(time.strftime("%H:%M:%S Local"))
class QWeatherWidget(QGroupBox):
"""
UI Component to display current weather forecast
"""
turn = None
conditions = None
def __init__(self):
super(QWeatherWidget, self).__init__("")
self.setProperty("style", "QWeatherWidget")
self.icons = {
TimeOfDay.Dawn: CONST.ICONS["Dawn"],
TimeOfDay.Day: CONST.ICONS["Day"],
TimeOfDay.Dusk: CONST.ICONS["Dusk"],
TimeOfDay.Night: CONST.ICONS["Night"],
}
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.makeWeatherIcon()
self.makeCloudRainFogWidget()
self.makeWindsWidget()
def makeWeatherIcon(self):
"""Makes the Weather Icon Widget"""
self.weather_icon = QLabel()
self.weather_icon.setPixmap(self.icons[TimeOfDay.Dawn])
self.layout.addWidget(self.weather_icon)
def makeCloudRainFogWidget(self):
"""Makes the Cloud, Rain, Fog Widget"""
self.textLayout = QVBoxLayout()
self.layout.addLayout(self.textLayout)
self.forecastClouds = self.makeLabel()
self.textLayout.addWidget(self.forecastClouds)
self.forecastRain = self.makeLabel()
self.textLayout.addWidget(self.forecastRain)
self.forecastFog = self.makeLabel()
self.textLayout.addWidget(self.forecastFog)
def makeWindsWidget(self):
"""Factory for the winds widget."""
windsLayout = QGridLayout()
self.layout.addLayout(windsLayout)
windsLayout.addWidget(self.makeIcon(CONST.ICONS["Weather_winds"]), 0, 0, 3, 1)
windsLayout.addWidget(self.makeLabel("At GL"), 0, 1)
windsLayout.addWidget(self.makeLabel("At FL08"), 1, 1)
windsLayout.addWidget(self.makeLabel("At FL26"), 2, 1)
self.windGLSpeedLabel = self.makeLabel("0kts")
self.windGLDirLabel = self.makeLabel("")
windsLayout.addWidget(self.windGLSpeedLabel, 0, 2)
windsLayout.addWidget(self.windGLDirLabel, 0, 3)
self.windFL08SpeedLabel = self.makeLabel("0kts")
self.windFL08DirLabel = self.makeLabel("")
windsLayout.addWidget(self.windFL08SpeedLabel, 1, 2)
windsLayout.addWidget(self.windFL08DirLabel, 1, 3)
self.windFL26SpeedLabel = self.makeLabel("0kts")
self.windFL26DirLabel = self.makeLabel("")
windsLayout.addWidget(self.windFL26SpeedLabel, 2, 2)
windsLayout.addWidget(self.windFL26DirLabel, 2, 3)
def makeLabel(self, text: str = "") -> QLabel:
"""Shorthand to generate a QLabel with widget standard style
:arg pixmap QPixmap for the icon.
"""
label = QLabel(text)
label.setProperty("style", "text-sm")
return label
def makeIcon(self, pixmap: QPixmap) -> QLabel:
"""Shorthand to generate a QIcon with pixmap.
:arg pixmap QPixmap for the icon.
"""
icon = QLabel()
icon.setPixmap(pixmap)
return icon
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.turn = turn
self.conditions = conditions
self.update_forecast()
self.updateWinds()
def updateWinds(self):
"""Updates the UI with the current conditions wind info."""
windGlSpeed = mps(self.conditions.weather.wind.at_0m.speed or 0)
windGlDir = str(self.conditions.weather.wind.at_0m.direction or 0).rjust(3, "0")
self.windGLSpeedLabel.setText(f"{int(windGlSpeed.knots)}kts")
self.windGLDirLabel.setText(f"{windGlDir}º")
windFL08Speed = mps(self.conditions.weather.wind.at_2000m.speed or 0)
windFL08Dir = str(self.conditions.weather.wind.at_2000m.direction or 0).rjust(
3, "0"
)
self.windFL08SpeedLabel.setText(f"{int(windFL08Speed.knots)}kts")
self.windFL08DirLabel.setText(f"{windFL08Dir}º")
windFL26Speed = mps(self.conditions.weather.wind.at_8000m.speed or 0)
windFL26Dir = str(self.conditions.weather.wind.at_8000m.direction or 0).rjust(
3, "0"
)
self.windFL26SpeedLabel.setText(f"{int(windFL26Speed.knots)}kts")
self.windFL26DirLabel.setText(f"{windFL26Dir}º")
def update_forecast_from_preset(self, preset: CloudPreset) -> None:
self.forecastFog.setText("No fog")
if "Rain" in preset.name:
self.forecastRain.setText("Rain")
self.update_forecast_icons("rain")
else:
self.forecastRain.setText("No rain")
self.update_forecast_icons("partly-cloudy")
# We get a description like the following for the cloud preset.
#
# 09 ##Two Layer Broken/Scattered \nMETAR:BKN 7.5/10 SCT 20/22 FEW41
#
# The second line is probably interesting but doesn't fit into the widget
# currently, so for now just extract the first line.
self.forecastClouds.setText(preset.description.splitlines()[0].split("##")[1])
def update_forecast(self):
"""Updates the Forecast Text and icon with the current conditions wind info."""
if (
self.conditions.weather.clouds
and self.conditions.weather.clouds.preset is not None
):
self.update_forecast_from_preset(self.conditions.weather.clouds.preset)
return
if self.conditions.weather.clouds is None:
cloud_density = 0
precipitation = None
else:
cloud_density = self.conditions.weather.clouds.density
precipitation = self.conditions.weather.clouds.precipitation
if not cloud_density:
self.forecastClouds.setText("Clear")
weather_type = "clear"
elif cloud_density < 3:
self.forecastClouds.setText("Partly Cloudy")
weather_type = "partly-cloudy"
elif cloud_density < 5:
self.forecastClouds.setText("Mostly Cloudy")
weather_type = "partly-cloudy"
else:
self.forecastClouds.setText("Totally Cloudy")
weather_type = "partly-cloudy"
if precipitation == PydcsWeather.Preceptions.Rain:
self.forecastRain.setText("Rain")
weather_type = "rain"
elif precipitation == PydcsWeather.Preceptions.Thunderstorm:
self.forecastRain.setText("Thunderstorm")
weather_type = "thunderstorm"
else:
self.forecastRain.setText("No rain")
if not self.conditions.weather.fog is not None:
self.forecastFog.setText("No fog")
else:
visibility = round(self.conditions.weather.fog.visibility.nautical_miles, 1)
self.forecastFog.setText(f"Fog vis: {visibility}nm")
if cloud_density > 1:
weather_type = "cloudy-fog"
else:
weather_type = "fog"
self.update_forecast_icons(weather_type)
def update_forecast_icons(self, weather_type: str) -> None:
time = "night" if self.conditions.time_of_day == TimeOfDay.Night else "day"
icon_key = f"Weather_{time}-{weather_type}"
icon = CONST.ICONS.get(icon_key) or CONST.ICONS["Weather_night-partly-cloudy"]
self.weather_icon.setPixmap(icon)
class QConditionsWidget(QFrame):
"""
UI Component to display Turn Number, Day Time & Hour and weather combined.
"""
def __init__(self, sim_controller: SimController) -> None:
super(QConditionsWidget, self).__init__()
self.setProperty("style", "QConditionsWidget")
self.layout = QGridLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setHorizontalSpacing(0)
self.layout.setVerticalSpacing(0)
self.setLayout(self.layout)
self.time_turn_widget = QTimeTurnWidget(sim_controller)
self.time_turn_widget.setStyleSheet("QGroupBox { margin-right: 0px; }")
self.layout.addWidget(self.time_turn_widget, 0, 0)
self.weather_widget = QWeatherWidget()
self.weather_widget.setStyleSheet(
"QGroupBox { margin-top: 5px; margin-left: 0px; border-left: 0px; }"
)
self.weather_widget.hide()
self.layout.addWidget(self.weather_widget, 0, 1)
def setCurrentTurn(self, turn: int, conditions: Conditions) -> None:
"""Sets the turn information display.
:arg turn Current turn number.
:arg conditions Current time and weather conditions.
"""
self.time_turn_widget.set_current_turn(turn, conditions)
self.weather_widget.setCurrentTurn(turn, conditions)
self.weather_widget.show()