diff --git a/qt_ui/widgets/map/model/holdzonesjs.py b/qt_ui/widgets/map/model/holdzonesjs.py index 33743d8a..1519b6ca 100644 --- a/qt_ui/widgets/map/model/holdzonesjs.py +++ b/qt_ui/widgets/map/model/holdzonesjs.py @@ -1,67 +1,33 @@ from __future__ import annotations -from PySide2.QtCore import Property, QObject, Signal +from pydantic import BaseModel, Field from game import Game from game.ato import Flight from game.flightplan import HoldZoneGeometry from .config import ENABLE_EXPENSIVE_DEBUG_TOOLS -from .leaflet import LeafletLatLon, LeafletPoly +from .leaflet import LeafletPoly from .shapelyutil import ShapelyUtil -class HoldZonesJs(QObject): - homeBubbleChanged = Signal() - targetBubbleChanged = Signal() - joinBubbleChanged = Signal() - excludedZonesChanged = Signal() - permissibleZonesChanged = Signal() - preferredLinesChanged = Signal() - - def __init__( - self, - home_bubble: LeafletPoly, - target_bubble: LeafletPoly, - join_bubble: LeafletPoly, - excluded_zones: list[LeafletPoly], - permissible_zones: list[LeafletPoly], - preferred_lines: list[list[LeafletLatLon]], - ) -> None: - super().__init__() - self._home_bubble = home_bubble - self._target_bubble = target_bubble - self._join_bubble = join_bubble - self._excluded_zones = excluded_zones - self._permissible_zones = permissible_zones - self._preferred_lines = preferred_lines - - @Property(list, notify=homeBubbleChanged) - def homeBubble(self) -> LeafletPoly: - return self._home_bubble - - @Property(list, notify=targetBubbleChanged) - def targetBubble(self) -> LeafletPoly: - return self._target_bubble - - @Property(list, notify=joinBubbleChanged) - def joinBubble(self) -> LeafletPoly: - return self._join_bubble - - @Property(list, notify=excludedZonesChanged) - def excludedZones(self) -> list[LeafletPoly]: - return self._excluded_zones - - @Property(list, notify=permissibleZonesChanged) - def permissibleZones(self) -> list[LeafletPoly]: - return self._permissible_zones - - @Property(list, notify=preferredLinesChanged) - def preferredLines(self) -> list[list[LeafletLatLon]]: - return self._preferred_lines +class HoldZonesJs(BaseModel): + home_bubble: LeafletPoly = Field(alias="homeBubble") + target_bubble: LeafletPoly = Field(alias="targetBubble") + join_bubble: LeafletPoly = Field(alias="joinBubble") + excluded_zones: list[LeafletPoly] = Field(alias="excludedZones") + permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones") + preferred_lines: list[LeafletPoly] = Field(alias="preferredLines") @classmethod def empty(cls) -> HoldZonesJs: - return HoldZonesJs([], [], [], [], [], []) + return HoldZonesJs( + homeBubble=[], + targetBubble=[], + joinBubble=[], + excludedZones=[], + permissibleZones=[], + preferredLines=[], + ) @classmethod def for_flight(cls, flight: Flight, game: Game) -> HoldZonesJs: @@ -77,10 +43,18 @@ class HoldZonesJs(QObject): target.position, home.position, ip, join, game.blue, game.theater ) return HoldZonesJs( - ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater), - ShapelyUtil.poly_to_leaflet(geometry.target_bubble, game.theater), - ShapelyUtil.poly_to_leaflet(geometry.join_bubble, game.theater), - ShapelyUtil.polys_to_leaflet(geometry.excluded_zones, game.theater), - ShapelyUtil.polys_to_leaflet(geometry.permissible_zones, game.theater), - ShapelyUtil.lines_to_leaflet(geometry.preferred_lines, game.theater), + homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater), + targetBubble=ShapelyUtil.poly_to_leaflet( + geometry.target_bubble, game.theater + ), + joinBubble=ShapelyUtil.poly_to_leaflet(geometry.join_bubble, game.theater), + excludedZones=ShapelyUtil.polys_to_leaflet( + geometry.excluded_zones, game.theater + ), + permissibleZones=ShapelyUtil.polys_to_leaflet( + geometry.permissible_zones, game.theater + ), + preferredLines=ShapelyUtil.lines_to_leaflet( + geometry.preferred_lines, game.theater + ), ) diff --git a/qt_ui/widgets/map/model/ipzonesjs.py b/qt_ui/widgets/map/model/ipzonesjs.py index 0698fe73..b756579b 100644 --- a/qt_ui/widgets/map/model/ipzonesjs.py +++ b/qt_ui/widgets/map/model/ipzonesjs.py @@ -1,6 +1,6 @@ from __future__ import annotations -from PySide2.QtCore import Property, QObject, Signal +from pydantic import BaseModel, Field from game import Game from game.ato import Flight @@ -10,44 +10,15 @@ from .leaflet import LeafletPoly from .shapelyutil import ShapelyUtil -class IpZonesJs(QObject): - homeBubbleChanged = Signal() - ipBubbleChanged = Signal() - permissibleZoneChanged = Signal() - safeZonesChanged = Signal() - - def __init__( - self, - home_bubble: LeafletPoly, - ip_bubble: LeafletPoly, - permissible_zone: LeafletPoly, - safe_zones: list[LeafletPoly], - ) -> None: - super().__init__() - self._home_bubble = home_bubble - self._ip_bubble = ip_bubble - self._permissible_zone = permissible_zone - self._safe_zones = safe_zones - - @Property(list, notify=homeBubbleChanged) - def homeBubble(self) -> LeafletPoly: - return self._home_bubble - - @Property(list, notify=ipBubbleChanged) - def ipBubble(self) -> LeafletPoly: - return self._ip_bubble - - @Property(list, notify=permissibleZoneChanged) - def permissibleZone(self) -> LeafletPoly: - return self._permissible_zone - - @Property(list, notify=safeZonesChanged) - def safeZones(self) -> list[LeafletPoly]: - return self._safe_zones +class IpZonesJs(BaseModel): + home_bubble: LeafletPoly = Field(alias="homeBubble") + ipBubble: LeafletPoly = Field(alias="ipBubble") + permissibleZone: LeafletPoly = Field(alias="permissibleZone") + safeZones: list[LeafletPoly] = Field(alias="safeZones") @classmethod def empty(cls) -> IpZonesJs: - return IpZonesJs([], [], [], []) + return IpZonesJs(homeBubble=[], ipBubble=[], permissibleZone=[], safeZones=[]) @classmethod def for_flight(cls, flight: Flight, game: Game) -> IpZonesJs: @@ -57,8 +28,10 @@ class IpZonesJs(QObject): home = flight.departure geometry = IpZoneGeometry(target.position, home.position, game.blue) return IpZonesJs( - ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater), - ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater), - ShapelyUtil.poly_to_leaflet(geometry.permissible_zone, game.theater), - ShapelyUtil.polys_to_leaflet(geometry.safe_zones, game.theater), + homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater), + ipBubble=ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater), + permissibleZone=ShapelyUtil.poly_to_leaflet( + geometry.permissible_zone, game.theater + ), + safeZones=ShapelyUtil.polys_to_leaflet(geometry.safe_zones, game.theater), ) diff --git a/qt_ui/widgets/map/model/joinzonesjs.py b/qt_ui/widgets/map/model/joinzonesjs.py index 245ac398..7a178753 100644 --- a/qt_ui/widgets/map/model/joinzonesjs.py +++ b/qt_ui/widgets/map/model/joinzonesjs.py @@ -1,67 +1,34 @@ from __future__ import annotations -from PySide2.QtCore import Property, QObject, Signal +from pydantic import Field +from pydantic.main import BaseModel from game import Game from game.ato import Flight from game.flightplan import JoinZoneGeometry from .config import ENABLE_EXPENSIVE_DEBUG_TOOLS -from .leaflet import LeafletLatLon, LeafletPoly +from .leaflet import LeafletPoly from .shapelyutil import ShapelyUtil -class JoinZonesJs(QObject): - homeBubbleChanged = Signal() - targetBubbleChanged = Signal() - ipBubbleChanged = Signal() - excludedZonesChanged = Signal() - permissibleZonesChanged = Signal() - preferredLinesChanged = Signal() - - def __init__( - self, - home_bubble: LeafletPoly, - target_bubble: LeafletPoly, - ip_bubble: LeafletPoly, - excluded_zones: list[LeafletPoly], - permissible_zones: list[LeafletPoly], - preferred_lines: list[list[LeafletLatLon]], - ) -> None: - super().__init__() - self._home_bubble = home_bubble - self._target_bubble = target_bubble - self._ip_bubble = ip_bubble - self._excluded_zones = excluded_zones - self._permissible_zones = permissible_zones - self._preferred_lines = preferred_lines - - @Property(list, notify=homeBubbleChanged) - def homeBubble(self) -> LeafletPoly: - return self._home_bubble - - @Property(list, notify=targetBubbleChanged) - def targetBubble(self) -> LeafletPoly: - return self._target_bubble - - @Property(list, notify=ipBubbleChanged) - def ipBubble(self) -> LeafletPoly: - return self._ip_bubble - - @Property(list, notify=excludedZonesChanged) - def excludedZones(self) -> list[LeafletPoly]: - return self._excluded_zones - - @Property(list, notify=permissibleZonesChanged) - def permissibleZones(self) -> list[LeafletPoly]: - return self._permissible_zones - - @Property(list, notify=preferredLinesChanged) - def preferredLines(self) -> list[list[LeafletLatLon]]: - return self._preferred_lines +class JoinZonesJs(BaseModel): + home_bubble: LeafletPoly = Field(alias="homeBubble") + target_bubble: LeafletPoly = Field(alias="targetBubble") + ip_bubble: LeafletPoly = Field(alias="ipBubble") + excluded_zones: list[LeafletPoly] = Field(alias="excludedZones") + permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones") + preferred_lines: list[LeafletPoly] = Field(alias="preferredLines") @classmethod def empty(cls) -> JoinZonesJs: - return JoinZonesJs([], [], [], [], [], []) + return JoinZonesJs( + homeBubble=[], + targetBubble=[], + ipBubble=[], + excludedZones=[], + permissibleZones=[], + preferredLines=[], + ) @classmethod def for_flight(cls, flight: Flight, game: Game) -> JoinZonesJs: @@ -74,10 +41,18 @@ class JoinZonesJs(QObject): ip = flight.package.waypoints.ingress geometry = JoinZoneGeometry(target.position, home.position, ip, game.blue) return JoinZonesJs( - ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater), - ShapelyUtil.poly_to_leaflet(geometry.target_bubble, game.theater), - ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater), - ShapelyUtil.polys_to_leaflet(geometry.excluded_zones, game.theater), - ShapelyUtil.polys_to_leaflet(geometry.permissible_zones, game.theater), - ShapelyUtil.lines_to_leaflet(geometry.preferred_lines, game.theater), + homeBubble=ShapelyUtil.poly_to_leaflet(geometry.home_bubble, game.theater), + targetBubble=ShapelyUtil.poly_to_leaflet( + geometry.target_bubble, game.theater + ), + ipBubble=ShapelyUtil.poly_to_leaflet(geometry.ip_bubble, game.theater), + excludedZones=ShapelyUtil.polys_to_leaflet( + geometry.excluded_zones, game.theater + ), + permissibleZones=ShapelyUtil.polys_to_leaflet( + geometry.permissible_zones, game.theater + ), + preferredLines=ShapelyUtil.lines_to_leaflet( + geometry.preferred_lines, game.theater + ), ) diff --git a/qt_ui/widgets/map/model/mapmodel.py b/qt_ui/widgets/map/model/mapmodel.py index e9d5b0e4..5d6a2679 100644 --- a/qt_ui/widgets/map/model/mapmodel.py +++ b/qt_ui/widgets/map/model/mapmodel.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging from typing import List, Optional, Tuple @@ -396,17 +397,19 @@ class MapModel(QObject): def unculledZones(self) -> list[UnculledZone]: return self._unculled_zones - @Property(IpZonesJs, notify=ipZonesChanged) - def ipZones(self) -> IpZonesJs: - return self._ip_zones + @Property(str, notify=ipZonesChanged) + def ipZones(self) -> str: + return json.dumps(self._ip_zones.dict(by_alias=True)) - @Property(JoinZonesJs, notify=joinZonesChanged) - def joinZones(self) -> JoinZonesJs: - return self._join_zones + @Property(str, notify=joinZonesChanged) + def joinZones(self) -> str: + # Must be dumped as a string and deserialized in js because QWebChannel can't + # handle a dict. Can be cleaned up by switching from QWebChannel to FastAPI. + return json.dumps(self._join_zones.dict(by_alias=True)) - @Property(HoldZonesJs, notify=holdZonesChanged) - def holdZones(self) -> HoldZonesJs: - return self._hold_zones + @Property(str, notify=holdZonesChanged) + def holdZones(self) -> str: + return json.dumps(self._hold_zones.dict(by_alias=True)) def reset_combats(self) -> None: self._air_combats = [] diff --git a/requirements.txt b/requirements.txt index 0fcaaeaf..cdb3d7ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ platformdirs==2.5.0 pluggy==1.0.0 pre-commit==2.17.0 py==1.11.0 +pydantic==1.9.0 -e git+https://github.com/pydcs/dcs@63863a88e0a43cb0a310dbab3ce2c7800a099dbb#egg=pydcs pyinstaller==4.9 pyinstaller-hooks-contrib==2022.1 diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index fa9447d9..5a1e0cd0 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -1106,25 +1106,27 @@ function drawIpZones() { return; } - L.polygon(game.ipZones.homeBubble, { + const iz = JSON.parse(game.ipZones); + + L.polygon(iz.homeBubble, { color: Colors.Highlight, fillOpacity: 0.1, interactive: false, }).addTo(ipZones); - L.polygon(game.ipZones.ipBubble, { + L.polygon(iz.ipBubble, { color: "#bb89ff", fillOpacity: 0.1, interactive: false, }).addTo(ipZones); - L.polygon(game.ipZones.permissibleZone, { + L.polygon(iz.permissibleZone, { color: "#ffffff", fillOpacity: 0.1, interactive: false, }).addTo(ipZones); - for (const zone of game.ipZones.safeZones) { + for (const zone of iz.safeZones) { L.polygon(zone, { color: Colors.Green, fillOpacity: 0.1, @@ -1140,25 +1142,27 @@ function drawJoinZones() { return; } - L.polygon(game.joinZones.homeBubble, { + const jz = JSON.parse(game.joinZones); + + L.polygon(jz.homeBubble, { color: Colors.Highlight, fillOpacity: 0.1, interactive: false, }).addTo(joinZones); - L.polygon(game.joinZones.targetBubble, { + L.polygon(jz.targetBubble, { color: "#bb89ff", fillOpacity: 0.1, interactive: false, }).addTo(joinZones); - L.polygon(game.joinZones.ipBubble, { + L.polygon(jz.ipBubble, { color: "#ffffff", fillOpacity: 0.1, interactive: false, }).addTo(joinZones); - for (const zone of game.joinZones.excludedZones) { + for (const zone of jz.excludedZones) { L.polygon(zone, { color: "#ffa500", fillOpacity: 0.2, @@ -1167,14 +1171,14 @@ function drawJoinZones() { }).addTo(joinZones); } - for (const zone of game.joinZones.permissibleZones) { + for (const zone of jz.permissibleZones) { L.polygon(zone, { color: Colors.Green, interactive: false, }).addTo(joinZones); } - for (const line of game.joinZones.preferredLines) { + for (const line of jz.preferredLines) { L.polyline(line, { color: Colors.Green, interactive: false, @@ -1189,25 +1193,27 @@ function drawHoldZones() { return; } - L.polygon(game.holdZones.homeBubble, { + const hz = JSON.parse(game.holdZones); + + L.polygon(hz.homeBubble, { color: Colors.Highlight, fillOpacity: 0.1, interactive: false, }).addTo(holdZones); - L.polygon(game.holdZones.targetBubble, { + L.polygon(hz.targetBubble, { color: Colors.Highlight, fillOpacity: 0.1, interactive: false, }).addTo(holdZones); - L.polygon(game.holdZones.joinBubble, { + L.polygon(hz.joinBubble, { color: Colors.Highlight, fillOpacity: 0.1, interactive: false, }).addTo(holdZones); - for (const zone of game.holdZones.excludedZones) { + for (const zone of hz.excludedZones) { L.polygon(zone, { color: "#ffa500", fillOpacity: 0.2, @@ -1216,14 +1222,14 @@ function drawHoldZones() { }).addTo(holdZones); } - for (const zone of game.holdZones.permissibleZones) { + for (const zone of hz.permissibleZones) { L.polygon(zone, { color: Colors.Green, interactive: false, }).addTo(holdZones); } - for (const line of game.holdZones.preferredLines) { + for (const line of hz.preferredLines) { L.polyline(line, { color: Colors.Green, interactive: false,