mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Add a new Leaflet based map UI.
This is extremely WIP. It is not usable for play yet. Enable with `--new-map`.
This commit is contained in:
parent
56abd0bb7f
commit
d9d68cd37c
@ -8,6 +8,7 @@ Saves from 2.5 are not compatible with 3.0.
|
||||
* **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them.
|
||||
* **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
|
||||
* **[Campaign AI]** Every 30 minutes the AI will plan a CAP, so players can customize their mission better.
|
||||
* **[UI]** Added (extremely WIP) new web based map UI. Enable with --new-map.
|
||||
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
|
||||
* **[Modding]** Campaigns now choose locations for factories to spawn.
|
||||
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
|
||||
|
||||
@ -478,12 +478,36 @@ class ConflictTheater:
|
||||
|
||||
def __init__(self):
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
self.point_to_ll_transformer = Transformer.from_crs(
|
||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||
)
|
||||
self.ll_to_point_transformer = Transformer.from_crs(
|
||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||
)
|
||||
"""
|
||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||
for x in self.landmap[1]:
|
||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
||||
"""
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
del state["point_to_ll_transformer"]
|
||||
del state["ll_to_point_transformer"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.point_to_ll_transformer = Transformer.from_crs(
|
||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||
)
|
||||
self.ll_to_point_transformer = Transformer.from_crs(
|
||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||
)
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint):
|
||||
self.controlpoints.append(point)
|
||||
|
||||
@ -637,35 +661,6 @@ class ConflictTheater:
|
||||
return i
|
||||
raise KeyError(f"Cannot find ControlPoint with ID {id}")
|
||||
|
||||
def add_json_cp(self, theater, p: dict) -> ControlPoint:
|
||||
cp: ControlPoint
|
||||
if p["type"] == "airbase":
|
||||
|
||||
airbase = theater.terrain.airports[p["id"]]
|
||||
|
||||
if "size" in p.keys():
|
||||
size = p["size"]
|
||||
else:
|
||||
size = SIZE_REGULAR
|
||||
|
||||
if "importance" in p.keys():
|
||||
importance = p["importance"]
|
||||
else:
|
||||
importance = IMPORTANCE_MEDIUM
|
||||
|
||||
cp = Airfield(airbase, size, importance)
|
||||
elif p["type"] == "carrier":
|
||||
cp = Carrier("carrier", Point(p["x"], p["y"]), p["id"])
|
||||
else:
|
||||
cp = Lha("lha", Point(p["x"], p["y"]), p["id"])
|
||||
|
||||
if "captured_invert" in p.keys():
|
||||
cp.captured_invert = p["captured_invert"]
|
||||
else:
|
||||
cp.captured_invert = False
|
||||
|
||||
return cp
|
||||
|
||||
@staticmethod
|
||||
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
||||
theaters = {
|
||||
@ -694,15 +689,11 @@ class ConflictTheater:
|
||||
raise NotImplementedError
|
||||
|
||||
def point_to_ll(self, point: Point) -> LatLon:
|
||||
lat, lon = Transformer.from_crs(
|
||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||
).transform(point.x, point.y)
|
||||
lat, lon = self.point_to_ll_transformer.transform(point.x, point.y)
|
||||
return LatLon(lat, lon)
|
||||
|
||||
def ll_to_point(self, ll: LatLon) -> Point:
|
||||
x, y = Transformer.from_crs(
|
||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||
).transform(ll.latitude, ll.longitude)
|
||||
x, y = self.ll_to_point_transformer.transform(ll.latitude, ll.longitude)
|
||||
return Point(x, y)
|
||||
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
||||
)
|
||||
|
||||
|
||||
def run_ui(game: Optional[Game] = None) -> None:
|
||||
def run_ui(game: Optional[Game], new_map: bool) -> None:
|
||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@ -104,7 +104,7 @@ def run_ui(game: Optional[Game] = None) -> None:
|
||||
GameUpdateSignal()
|
||||
|
||||
# Start window
|
||||
window = QLiberationWindow(game)
|
||||
window = QLiberationWindow(game, new_map)
|
||||
window.showMaximized()
|
||||
splash.finish(window)
|
||||
qt_execution_code = app.exec_()
|
||||
@ -132,6 +132,10 @@ def parse_args() -> argparse.Namespace:
|
||||
help="Emits a warning for weapons without date or fallback information.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--new-map", action="store_true", help="Use the new map. Non functional."
|
||||
)
|
||||
|
||||
new_game = subparsers.add_parser("new-game")
|
||||
|
||||
new_game.add_argument(
|
||||
@ -239,7 +243,7 @@ def main():
|
||||
args.cheats,
|
||||
)
|
||||
|
||||
run_ui(game)
|
||||
run_ui(game, args.new_map)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -4,7 +4,15 @@ import datetime
|
||||
import logging
|
||||
import math
|
||||
from functools import singledispatchmethod
|
||||
from typing import Iterable, Iterator, List, Optional, Sequence, Tuple
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
from PySide2.QtCore import QLineF, QPointF, QRectF, Qt
|
||||
@ -17,6 +25,11 @@ from PySide2.QtGui import (
|
||||
QPolygonF,
|
||||
QWheelEvent,
|
||||
)
|
||||
from PySide2.QtWebChannel import QWebChannel
|
||||
from PySide2.QtWebEngineWidgets import (
|
||||
QWebEnginePage,
|
||||
QWebEngineView,
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGraphicsItem,
|
||||
@ -40,7 +53,10 @@ import qt_ui.uiconstants as CONST
|
||||
from game import Game
|
||||
from game.navmesh import NavMesh
|
||||
from game.theater import ControlPoint, Enum
|
||||
from game.theater.conflicttheater import FrontLine, ReferencePoint
|
||||
from game.theater.conflicttheater import (
|
||||
FrontLine,
|
||||
ReferencePoint,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
@ -69,6 +85,7 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||
from qt_ui.widgets.map.ShippingLaneSegment import ShippingLaneSegment
|
||||
from qt_ui.widgets.map.SupplyRouteSegment import SupplyRouteSegment
|
||||
from qt_ui.widgets.map.mapmodel import MapModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
|
||||
MAX_SHIP_DISTANCE = nautical_miles(80)
|
||||
@ -112,17 +129,65 @@ class QLiberationMapState(Enum):
|
||||
MOVING_UNIT = 1
|
||||
|
||||
|
||||
class QLiberationMap(QGraphicsView):
|
||||
class LoggingWebPage(QWebEnginePage):
|
||||
def javaScriptConsoleMessage(
|
||||
self,
|
||||
level: QWebEnginePage.JavaScriptConsoleMessageLevel,
|
||||
message: str,
|
||||
line_number: int,
|
||||
source: str,
|
||||
) -> None:
|
||||
if level == QWebEnginePage.JavaScriptConsoleMessageLevel.ErrorMessageLevel:
|
||||
logging.error(message)
|
||||
elif level == QWebEnginePage.JavaScriptConsoleMessageLevel.WarningMessageLevel:
|
||||
logging.warning(message)
|
||||
else:
|
||||
logging.info(message)
|
||||
|
||||
|
||||
class LiberationMap:
|
||||
def set_game(self, game: Optional[Game]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LeafletMap(QWebEngineView, LiberationMap):
|
||||
def __init__(self, game_model: GameModel, parent) -> None:
|
||||
super().__init__(parent)
|
||||
self.game_model = game_model
|
||||
self.setMinimumSize(800, 600)
|
||||
self.map_model = MapModel(game_model)
|
||||
|
||||
self.channel = QWebChannel()
|
||||
self.channel.registerObject("game", self.map_model)
|
||||
|
||||
self.page = LoggingWebPage(self)
|
||||
self.page.setWebChannel(self.channel)
|
||||
self.page.setHtml(Path("resources/ui/map/canvas.html").read_text())
|
||||
self.setPage(self.page)
|
||||
|
||||
self.loadFinished.connect(self.load_finished)
|
||||
|
||||
def load_finished(self) -> None:
|
||||
self.page.runJavaScript(Path("resources/ui/map/map.js").read_text())
|
||||
|
||||
def set_game(self, game: Optional[Game]) -> None:
|
||||
if game is None:
|
||||
self.map_model.clear()
|
||||
else:
|
||||
self.map_model.reset()
|
||||
|
||||
|
||||
class QLiberationMap(QGraphicsView, LiberationMap):
|
||||
|
||||
WAYPOINT_SIZE = 4
|
||||
reference_point_setup_mode = False
|
||||
instance: Optional[QLiberationMap] = None
|
||||
|
||||
def __init__(self, game_model: GameModel):
|
||||
super(QLiberationMap, self).__init__()
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
QLiberationMap.instance = self
|
||||
self.game_model = game_model
|
||||
self.game: Optional[Game] = None # Setup by setGame below.
|
||||
self.game: Optional[Game] = None # Setup by set_game below.
|
||||
self.state = QLiberationMapState.NORMAL
|
||||
|
||||
self.waypoint_info_font = QFont()
|
||||
@ -138,7 +203,7 @@ class QLiberationMap(QGraphicsView):
|
||||
self.factor = 1
|
||||
self.factorized = 1
|
||||
self.init_scene()
|
||||
self.setGame(game_model.game)
|
||||
self.set_game(game_model.game)
|
||||
|
||||
# Object displayed when unit is selected
|
||||
self.movement_line = QtWidgets.QGraphicsLineItem(
|
||||
@ -201,7 +266,7 @@ class QLiberationMap(QGraphicsView):
|
||||
self.setFrameShape(QFrame.NoFrame)
|
||||
self.setDragMode(QGraphicsView.ScrollHandDrag)
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
def set_game(self, game: Optional[Game]):
|
||||
should_recenter = self.game is None
|
||||
self.game = game
|
||||
if self.game is not None:
|
||||
|
||||
323
qt_ui/widgets/map/mapmodel.py
Normal file
323
qt_ui/widgets/map/mapmodel.py
Normal file
@ -0,0 +1,323 @@
|
||||
import logging
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
from dcs import Point
|
||||
|
||||
from game import Game
|
||||
from game.profiling import logged_duration
|
||||
from game.theater import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.flights.flight import Flight, FlightWaypoint
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
|
||||
|
||||
LeafletLatLon = List[float]
|
||||
|
||||
|
||||
class ControlPointJs(QObject):
|
||||
def __init__(
|
||||
self,
|
||||
control_point: ControlPoint,
|
||||
game_model: GameModel,
|
||||
theater: ConflictTheater,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.control_point = control_point
|
||||
self.game_model = game_model
|
||||
self.theater = theater
|
||||
|
||||
@Property(str)
|
||||
def name(self) -> str:
|
||||
return self.control_point.name
|
||||
|
||||
@Property(bool)
|
||||
def blue(self) -> bool:
|
||||
return self.control_point.captured
|
||||
|
||||
@Property(list)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.control_point.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
|
||||
@Slot()
|
||||
def open_base_menu(self) -> None:
|
||||
self.base_details_dialog = QBaseMenu2(None, self.control_point, self.game_model)
|
||||
self.base_details_dialog.show()
|
||||
|
||||
|
||||
class GroundObjectJs(QObject):
|
||||
def __init__(self, tgo: TheaterGroundObject, theater: ConflictTheater) -> None:
|
||||
super().__init__()
|
||||
self.tgo = tgo
|
||||
self.theater = theater
|
||||
|
||||
@Property(bool)
|
||||
def blue(self) -> bool:
|
||||
return self.tgo.control_point.captured
|
||||
|
||||
@Property(list)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.tgo.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
|
||||
@Property(list)
|
||||
def samThreatRanges(self) -> List[float]:
|
||||
if not self.tgo.might_have_aa:
|
||||
return []
|
||||
|
||||
ranges = []
|
||||
for group in self.tgo.groups:
|
||||
threat_range = self.tgo.threat_range(group)
|
||||
if threat_range:
|
||||
ranges.append(threat_range.meters)
|
||||
return ranges
|
||||
|
||||
@Property(list)
|
||||
def samDetectionRanges(self) -> List[float]:
|
||||
if not self.tgo.might_have_aa:
|
||||
return []
|
||||
|
||||
ranges = []
|
||||
for group in self.tgo.groups:
|
||||
detection_range = self.tgo.detection_range(group)
|
||||
if detection_range:
|
||||
ranges.append(detection_range.meters)
|
||||
return ranges
|
||||
|
||||
|
||||
class SupplyRouteJs(QObject):
|
||||
def __init__(self, points: List[LeafletLatLon]) -> None:
|
||||
super().__init__()
|
||||
self._points = points
|
||||
|
||||
@Property(list)
|
||||
def points(self) -> List[LeafletLatLon]:
|
||||
return self._points
|
||||
|
||||
|
||||
class WaypointJs(QObject):
|
||||
def __init__(self, waypoint: FlightWaypoint, theater: ConflictTheater) -> None:
|
||||
super().__init__()
|
||||
self.waypoint = waypoint
|
||||
self.theater = theater
|
||||
|
||||
@Property(list)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.waypoint.position)
|
||||
return [ll.latitude, ll.longitude]
|
||||
|
||||
|
||||
class FlightJs(QObject):
|
||||
flightPlanChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self, flight: Flight, selected: bool, theater: ConflictTheater
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self._selected = selected
|
||||
self.theater = theater
|
||||
self._waypoints = []
|
||||
self.reset_waypoints()
|
||||
|
||||
def reset_waypoints(self) -> None:
|
||||
self._waypoints = [WaypointJs(p, self.theater) for p in self.flight.points]
|
||||
self.flightPlanChanged.emit()
|
||||
|
||||
@Property(list, notify=flightPlanChanged)
|
||||
def flightPlan(self) -> List[WaypointJs]:
|
||||
return self._waypoints
|
||||
|
||||
@Property(bool)
|
||||
def blue(self) -> bool:
|
||||
return self.flight.departure.captured
|
||||
|
||||
@Property(bool)
|
||||
def selected(self) -> bool:
|
||||
return self._selected
|
||||
|
||||
|
||||
class MapModel(QObject):
|
||||
cleared = Signal()
|
||||
|
||||
mapCenterChanged = Signal(list)
|
||||
controlPointsChanged = Signal()
|
||||
groundObjectsChanged = Signal()
|
||||
supplyRoutesChanged = Signal()
|
||||
flightsChanged = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
self.game_model = game_model
|
||||
self._map_center = [0, 0]
|
||||
self._control_points = []
|
||||
self._ground_objects = []
|
||||
self._supply_routes = []
|
||||
self._flights = []
|
||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||
GameUpdateSignal.get_instance().package_selection_changed.connect(
|
||||
self.set_package_selection
|
||||
)
|
||||
GameUpdateSignal.get_instance().flight_selection_changed.connect(
|
||||
self.set_flight_selection
|
||||
)
|
||||
self.reset()
|
||||
|
||||
def set_package_selection(self, index: int) -> None:
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be converted to
|
||||
# zero automatically. We use -1 to indicate no selection.
|
||||
if index == -1:
|
||||
self._selected_flight_index = None
|
||||
else:
|
||||
self._selected_flight_index = index, 0
|
||||
self.reset_atos()
|
||||
|
||||
def set_flight_selection(self, index: int) -> None:
|
||||
if self._selected_flight_index is None:
|
||||
if index != -1:
|
||||
# We don't know what order update_package_selection and
|
||||
# update_flight_selection will be called in when the last
|
||||
# package is removed. If no flight is selected, it's not a
|
||||
# problem to also have no package selected.
|
||||
logging.error("Flight was selected with no package selected")
|
||||
return
|
||||
|
||||
# Optional[int] isn't a valid type for a Qt signal. None will be converted to
|
||||
# zero automatically. We use -1 to indicate no selection.
|
||||
if index == -1:
|
||||
self._selected_flight_index = self._selected_flight_index[0], None
|
||||
self._selected_flight_index = self._selected_flight_index[0], index
|
||||
self.reset_atos()
|
||||
|
||||
@staticmethod
|
||||
def leaflet_coord_for(point: Point, theater: ConflictTheater) -> LeafletLatLon:
|
||||
ll = theater.point_to_ll(point)
|
||||
return [ll.latitude, ll.longitude]
|
||||
|
||||
def reset(self) -> None:
|
||||
if self.game_model.game is None:
|
||||
self.clear()
|
||||
return
|
||||
with logged_duration("Map reset"):
|
||||
self.reset_control_points()
|
||||
self.reset_ground_objects()
|
||||
self.reset_routes()
|
||||
self.reset_atos()
|
||||
|
||||
def on_game_load(self, game: Optional[Game]) -> None:
|
||||
if game is not None:
|
||||
self.reset_map_center(game.theater)
|
||||
|
||||
def reset_map_center(self, theater: ConflictTheater) -> None:
|
||||
ll = theater.point_to_ll(theater.terrain.map_view_default.position)
|
||||
self._map_center = [ll.latitude, ll.longitude]
|
||||
self.mapCenterChanged.emit(self._map_center)
|
||||
|
||||
@Property(list, notify=mapCenterChanged)
|
||||
def mapCenter(self) -> LeafletLatLon:
|
||||
return self._map_center
|
||||
|
||||
def _flights_in_ato(self, ato: AirTaskingOrder, blue: bool) -> List[FlightJs]:
|
||||
flights = []
|
||||
for p_idx, package in enumerate(ato.packages):
|
||||
for f_idx, flight in enumerate(package.flights):
|
||||
flights.append(
|
||||
FlightJs(
|
||||
flight,
|
||||
selected=blue and (p_idx, f_idx) == self._selected_flight_index,
|
||||
theater=self.game.theater,
|
||||
)
|
||||
)
|
||||
return flights
|
||||
|
||||
def reset_atos(self) -> None:
|
||||
self._flights = self._flights_in_ato(
|
||||
self.game.blue_ato, blue=True
|
||||
) + self._flights_in_ato(self.game.red_ato, blue=False)
|
||||
self.flightsChanged.emit()
|
||||
|
||||
@Property(list, notify=flightsChanged)
|
||||
def flights(self) -> List[FlightJs]:
|
||||
return self._flights
|
||||
|
||||
def reset_control_points(self) -> None:
|
||||
self._control_points = [
|
||||
ControlPointJs(c, self.game_model, self.game.theater)
|
||||
for c in self.game.theater.controlpoints
|
||||
]
|
||||
self.controlPointsChanged.emit()
|
||||
|
||||
@Property(list, notify=controlPointsChanged)
|
||||
def controlPoints(self) -> List[ControlPointJs]:
|
||||
return self._control_points
|
||||
|
||||
def reset_ground_objects(self) -> None:
|
||||
seen = set()
|
||||
self._ground_objects = []
|
||||
for cp in self.game.theater.controlpoints:
|
||||
for tgo in cp.ground_objects:
|
||||
if tgo.name in seen:
|
||||
continue
|
||||
seen.add(tgo.name)
|
||||
|
||||
self._ground_objects.append(GroundObjectJs(tgo, self.game.theater))
|
||||
self.groundObjectsChanged.emit()
|
||||
|
||||
@Property(list, notify=groundObjectsChanged)
|
||||
def groundObjects(self) -> List[GroundObjectJs]:
|
||||
return self._ground_objects
|
||||
|
||||
def reset_routes(self) -> None:
|
||||
seen = set()
|
||||
self._supply_routes = []
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
seen.add(control_point)
|
||||
for destination, convoy_route in control_point.convoy_routes.items():
|
||||
if destination in seen:
|
||||
continue
|
||||
self._supply_routes.append(
|
||||
SupplyRouteJs(
|
||||
[
|
||||
self.leaflet_coord_for(p, self.game.theater)
|
||||
for p in convoy_route
|
||||
]
|
||||
)
|
||||
)
|
||||
for destination, shipping_lane in control_point.shipping_lanes.items():
|
||||
if destination in seen:
|
||||
continue
|
||||
if control_point.is_friendly(destination.captured):
|
||||
self._supply_routes.append(
|
||||
SupplyRouteJs(
|
||||
[
|
||||
self.leaflet_coord_for(p, self.game.theater)
|
||||
for p in shipping_lane
|
||||
]
|
||||
)
|
||||
)
|
||||
self.supplyRoutesChanged.emit()
|
||||
|
||||
@Property(list, notify=supplyRoutesChanged)
|
||||
def supplyRoutes(self) -> List[SupplyRouteJs]:
|
||||
return self._supply_routes
|
||||
|
||||
def clear(self) -> None:
|
||||
self._control_points = []
|
||||
self._supply_routes = []
|
||||
self._ground_objects = []
|
||||
self._flights = []
|
||||
self.cleared.emit()
|
||||
|
||||
@property
|
||||
def game(self) -> Game:
|
||||
if self.game_model.game is None:
|
||||
raise RuntimeError("No game loaded")
|
||||
return self.game_model.game
|
||||
@ -15,6 +15,8 @@ class GameUpdateSignal(QObject):
|
||||
budgetupdated = Signal(Game)
|
||||
debriefingReceived = Signal(Debriefing)
|
||||
|
||||
game_loaded = Signal(Game)
|
||||
|
||||
flight_paths_changed = Signal()
|
||||
package_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
flight_selection_changed = Signal(int) # -1 indicates no selection.
|
||||
@ -23,6 +25,8 @@ class GameUpdateSignal(QObject):
|
||||
super(GameUpdateSignal, self).__init__()
|
||||
GameUpdateSignal.instance = self
|
||||
|
||||
self.game_loaded.connect(self.updateGame)
|
||||
|
||||
def select_package(self, index: Optional[int]) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_selection_changed.emit(-1 if index is None else index)
|
||||
|
||||
@ -27,7 +27,7 @@ from qt_ui.models import GameModel
|
||||
from qt_ui.uiconstants import URLS
|
||||
from qt_ui.widgets.QTopPanel import QTopPanel
|
||||
from qt_ui.widgets.ato import QAirTaskingOrderPanel
|
||||
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
|
||||
from qt_ui.widgets.map.QLiberationMap import LeafletMap, QLiberationMap, LiberationMap
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
|
||||
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
|
||||
@ -38,7 +38,7 @@ from qt_ui.windows.preferences.QLiberationPreferencesWindow import (
|
||||
|
||||
|
||||
class QLiberationWindow(QMainWindow):
|
||||
def __init__(self, game: Optional[Game]) -> None:
|
||||
def __init__(self, game: Optional[Game], new_map: bool) -> None:
|
||||
super(QLiberationWindow, self).__init__()
|
||||
|
||||
self.game = game
|
||||
@ -46,7 +46,7 @@ class QLiberationWindow(QMainWindow):
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
self.info_panel = QInfoPanel(self.game)
|
||||
self.liberation_map = QLiberationMap(self.game_model)
|
||||
self.liberation_map: LiberationMap = self.create_map(new_map)
|
||||
|
||||
self.setGeometry(300, 100, 270, 100)
|
||||
self.setWindowTitle(f"DCS Liberation - v{VERSION}")
|
||||
@ -254,14 +254,13 @@ class QLiberationWindow(QMainWindow):
|
||||
)
|
||||
if file is not None:
|
||||
game = persistency.load_game(file[0])
|
||||
GameUpdateSignal.get_instance().updateGame(game)
|
||||
GameUpdateSignal.get_instance().game_loaded.emit(game)
|
||||
|
||||
def saveGame(self):
|
||||
logging.info("Saving game")
|
||||
|
||||
if self.game.savepath:
|
||||
persistency.save_game(self.game)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
liberation_install.setup_last_save_file(self.game.savepath)
|
||||
liberation_install.save_config()
|
||||
else:
|
||||
@ -283,7 +282,12 @@ class QLiberationWindow(QMainWindow):
|
||||
def onGameGenerated(self, game: Game):
|
||||
logging.info("On Game generated")
|
||||
self.game = game
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
GameUpdateSignal.get_instance().game_loaded.emit(self.game)
|
||||
|
||||
def create_map(self, new_map: bool) -> LiberationMap:
|
||||
if new_map:
|
||||
return LeafletMap(self.game_model, self)
|
||||
return QLiberationMap(self.game_model)
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
try:
|
||||
@ -291,8 +295,7 @@ class QLiberationWindow(QMainWindow):
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
self.game_model.set(self.game)
|
||||
if self.liberation_map is not None:
|
||||
self.liberation_map.setGame(game)
|
||||
self.liberation_map.set_game(game)
|
||||
except AttributeError:
|
||||
logging.exception("Incompatible save game")
|
||||
QMessageBox.critical(
|
||||
|
||||
@ -1,22 +1,19 @@
|
||||
from PySide2.QtWidgets import QGroupBox, QGridLayout, QLabel, QHBoxLayout, QVBoxLayout
|
||||
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
|
||||
|
||||
from gen.flights.flight import FlightWaypoint
|
||||
|
||||
|
||||
class QFlightWaypointInfoBox(QGroupBox):
|
||||
def __init__(self, flight_wpt: FlightWaypoint = None):
|
||||
def __init__(self) -> None:
|
||||
super(QFlightWaypointInfoBox, self).__init__("Waypoint")
|
||||
self.flight_wpt = flight_wpt
|
||||
if flight_wpt is None:
|
||||
self.flight_wpt = FlightWaypoint(0, 0, 0)
|
||||
self.x_position_label = QLabel(str(self.flight_wpt.x))
|
||||
self.y_position_label = QLabel(str(self.flight_wpt.y))
|
||||
self.alt_label = QLabel(str(int(self.flight_wpt.alt.feet)))
|
||||
self.name_label = QLabel(str(self.flight_wpt.name))
|
||||
self.desc_label = QLabel(str(self.flight_wpt.description))
|
||||
self.x_position_label = QLabel("0")
|
||||
self.y_position_label = QLabel("0")
|
||||
self.alt_label = QLabel("0")
|
||||
self.name_label = QLabel("")
|
||||
self.desc_label = QLabel("")
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
def init_ui(self) -> None:
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
@ -53,13 +50,10 @@ class QFlightWaypointInfoBox(QGroupBox):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def set_flight_waypoint(self, flight_wpt: FlightWaypoint):
|
||||
self.flight_wpt = flight_wpt
|
||||
if flight_wpt is None:
|
||||
self.flight_wpt = FlightWaypoint(0, 0, 0)
|
||||
self.x_position_label.setText(str(self.flight_wpt.x))
|
||||
self.y_position_label.setText(str(self.flight_wpt.y))
|
||||
self.alt_label.setText(str(int(self.flight_wpt.alt.feet)))
|
||||
self.name_label.setText(str(self.flight_wpt.name))
|
||||
self.desc_label.setText(str(self.flight_wpt.description))
|
||||
self.setTitle(self.flight_wpt.name)
|
||||
def set_flight_waypoint(self, flight_wpt: FlightWaypoint) -> None:
|
||||
self.x_position_label.setText(str(flight_wpt.x))
|
||||
self.y_position_label.setText(str(flight_wpt.y))
|
||||
self.alt_label.setText(str(int(flight_wpt.alt.feet)))
|
||||
self.name_label.setText(str(flight_wpt.name))
|
||||
self.desc_label.setText(str(flight_wpt.description))
|
||||
self.setTitle(flight_wpt.name)
|
||||
|
||||
35
resources/ui/map/canvas.html
Normal file
35
resources/ui/map/canvas.html
Normal file
@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DCS Liberation Map</title>
|
||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
|
||||
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
|
||||
crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
|
||||
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
|
||||
crossorigin=""></script>
|
||||
|
||||
<script src="https://unpkg.com/esri-leaflet@3.0.1/dist/esri-leaflet.js"
|
||||
integrity="sha512-JmpptMCcCg+Rd6x0Dbg6w+mmyzs1M7chHCd9W8HPovnImG2nLAQWn3yltwxXRM7WjKKFFHOAKjjF2SC4CgiFBg=="
|
||||
crossorigin=""></script>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet-groupedlayercontrol@0.6.1/dist/leaflet.groupedlayercontrol.min.css"
|
||||
crossorigin="sha384-jTaPj8/7LfdZXicEJ/SUWan4gkbYw6lXyoRUNPMmNV3fYVfBzHJK8wD6A8Djh+3A">
|
||||
<script
|
||||
src="https://unpkg.com/leaflet-groupedlayercontrol@0.6.1/dist/leaflet.groupedlayercontrol.min.js"
|
||||
integrity="sha384-XAr1poM2RCR9/QQFki7ylrGSdmvYE0NuHghuRuxb/k9zJQA53y6qR5te5jJRZlcL"
|
||||
crossorigin="">
|
||||
</script>
|
||||
<style>
|
||||
body { padding: 0; margin: 0; }
|
||||
html, body, #map { height: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
</body>
|
||||
</html>
|
||||
220
resources/ui/map/map.js
Normal file
220
resources/ui/map/map.js
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* TODO:
|
||||
*
|
||||
* - Culling
|
||||
* - Threat zones
|
||||
* - Navmeshes
|
||||
* - CV waypoints
|
||||
* - Time of day/weather themeing
|
||||
* - Exclusion zones
|
||||
* - Commit ranges
|
||||
* - Waypoint info
|
||||
* - Supply route status
|
||||
* - Front line
|
||||
* - Debug flight plan drawing
|
||||
* - Icon variety
|
||||
*/
|
||||
|
||||
const Colors = Object.freeze({
|
||||
Blue: "#0084ff",
|
||||
Red: "#c85050",
|
||||
});
|
||||
|
||||
var map = L.map("map").setView([0, 0], 3);
|
||||
|
||||
// https://esri.github.io/esri-leaflet/api-reference/layers/basemap-layer.html
|
||||
var baseLayers = {
|
||||
"Imagery Clarity": L.esri.basemapLayer("ImageryClarity", { maxZoom: 17 }),
|
||||
"Imagery Firefly": L.esri.basemapLayer("ImageryFirefly", { maxZoom: 17 }),
|
||||
};
|
||||
|
||||
var defaultBaseMap = baseLayers["Imagery Clarity"];
|
||||
defaultBaseMap.addTo(map);
|
||||
|
||||
// Enabled by default, so addTo(map).
|
||||
var controlPointsLayer = L.layerGroup().addTo(map);
|
||||
var groundObjectsLayer = L.layerGroup().addTo(map);
|
||||
var supplyRoutesLayer = L.layerGroup().addTo(map);
|
||||
var redSamThreatLayer = L.layerGroup().addTo(map);
|
||||
var blueFlightPlansLayer = L.layerGroup().addTo(map);
|
||||
|
||||
// Added to map by the user via layer controls.
|
||||
var blueSamThreatLayer = L.layerGroup();
|
||||
var blueSamDetectionLayer = L.layerGroup();
|
||||
var redSamDetectionLayer = L.layerGroup();
|
||||
var redFlightPlansLayer = L.layerGroup();
|
||||
var selectedFlightPlansLayer = L.layerGroup();
|
||||
|
||||
L.control
|
||||
.groupedLayers(
|
||||
baseLayers,
|
||||
{
|
||||
"Points of Interest": {
|
||||
"Control points": controlPointsLayer,
|
||||
"Ground objects": groundObjectsLayer,
|
||||
"Supply routes": supplyRoutesLayer,
|
||||
},
|
||||
"Air Defenses": {
|
||||
"Ally SAM threat range": blueSamThreatLayer,
|
||||
"Enemy SAM threat range": redSamThreatLayer,
|
||||
"Ally SAM detection range": blueSamDetectionLayer,
|
||||
"Enemy SAM detection range": redSamDetectionLayer,
|
||||
},
|
||||
"Flight Plans": {
|
||||
"Hide": L.layerGroup(),
|
||||
"Show selected blue": selectedFlightPlansLayer,
|
||||
"Show all blue": blueFlightPlansLayer,
|
||||
"Show all red": redFlightPlansLayer,
|
||||
},
|
||||
},
|
||||
{ collapsed: false, exclusiveGroups: ["Flight Plans"] }
|
||||
)
|
||||
.addTo(map);
|
||||
|
||||
var friendlyCpIcon = new L.Icon({
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png",
|
||||
shadowUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png",
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
|
||||
var enemyCpIcon = new L.Icon({
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png",
|
||||
shadowUrl:
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png",
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
|
||||
var game;
|
||||
new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
game = channel.objects.game;
|
||||
drawInitialMap();
|
||||
game.cleared.connect(clearAllLayers);
|
||||
game.mapCenterChanged.connect(recenterMap);
|
||||
game.controlPointsChanged.connect(drawControlPoints);
|
||||
game.groundObjectsChanged.connect(drawGroundObjects);
|
||||
game.supplyRoutesChanged.connect(drawSupplyRoutes);
|
||||
game.flightsChanged.connect(drawFlightPlans);
|
||||
});
|
||||
|
||||
function recenterMap(center) {
|
||||
map.setView(center, 8, { animate: true, duration: 1 });
|
||||
}
|
||||
|
||||
function iconFor(player) {
|
||||
if (player) {
|
||||
return friendlyCpIcon;
|
||||
} else {
|
||||
return enemyCpIcon;
|
||||
}
|
||||
}
|
||||
|
||||
function drawControlPoints() {
|
||||
controlPointsLayer.clearLayers();
|
||||
game.controlPoints.forEach((cp) => {
|
||||
L.marker(cp.position, { icon: iconFor(cp.blue) })
|
||||
.on("click", function () {
|
||||
cp.open_base_menu();
|
||||
})
|
||||
.addTo(controlPointsLayer);
|
||||
});
|
||||
}
|
||||
|
||||
function drawSamThreatsAt(tgo) {
|
||||
var detectionLayer = tgo.blue ? blueSamDetectionLayer : redSamDetectionLayer;
|
||||
var threatLayer = tgo.blue ? blueSamThreatLayer : redSamThreatLayer;
|
||||
var threatColor = tgo.blue ? Colors.Blue : Colors.Red;
|
||||
var detectionColor = tgo.blue ? "#bb89ff" : "#eee17b";
|
||||
|
||||
tgo.samDetectionRanges.forEach((range) => {
|
||||
L.circle(tgo.position, {
|
||||
radius: range,
|
||||
color: detectionColor,
|
||||
fill: false,
|
||||
weight: 2,
|
||||
}).addTo(detectionLayer);
|
||||
});
|
||||
|
||||
tgo.samThreatRanges.forEach((range) => {
|
||||
L.circle(tgo.position, {
|
||||
radius: range,
|
||||
color: threatColor,
|
||||
fill: false,
|
||||
weight: 2,
|
||||
}).addTo(threatLayer);
|
||||
});
|
||||
}
|
||||
|
||||
function drawGroundObjects() {
|
||||
groundObjectsLayer.clearLayers();
|
||||
blueSamDetectionLayer.clearLayers();
|
||||
redSamDetectionLayer.clearLayers();
|
||||
blueSamThreatLayer.clearLayers();
|
||||
redSamThreatLayer.clearLayers();
|
||||
game.groundObjects.forEach((tgo) => {
|
||||
L.marker(tgo.position, { icon: iconFor(tgo.blue) }).addTo(
|
||||
groundObjectsLayer
|
||||
);
|
||||
drawSamThreatsAt(tgo);
|
||||
});
|
||||
}
|
||||
|
||||
function drawSupplyRoutes() {
|
||||
supplyRoutesLayer.clearLayers();
|
||||
game.supplyRoutes.forEach((route) => {
|
||||
L.polyline(route.points).addTo(supplyRoutesLayer);
|
||||
});
|
||||
}
|
||||
|
||||
function drawFlightPlan(flight) {
|
||||
var layer = flight.blue ? blueFlightPlansLayer : redFlightPlansLayer;
|
||||
var color = flight.blue ? Colors.Blue : Colors.Red;
|
||||
var highlight = "#ffff00";
|
||||
var points = [];
|
||||
flight.flightPlan.forEach((waypoint) => {
|
||||
points.push(waypoint.position);
|
||||
L.circle(waypoint.position, { radius: 50, color: color }).addTo(layer);
|
||||
if (flight.selected) {
|
||||
L.circle(waypoint.position, { radius: 50, color: highlight }).addTo(
|
||||
selectedFlightPlansLayer
|
||||
);
|
||||
}
|
||||
});
|
||||
L.polyline(points, { color: color }).addTo(layer);
|
||||
if (flight.selected) {
|
||||
L.polyline(points, { color: highlight }).addTo(selectedFlightPlansLayer);
|
||||
}
|
||||
}
|
||||
|
||||
function drawFlightPlans() {
|
||||
blueFlightPlansLayer.clearLayers();
|
||||
redFlightPlansLayer.clearLayers();
|
||||
selectedFlightPlansLayer.clearLayers();
|
||||
game.flights.forEach((flight) => {
|
||||
drawFlightPlan(flight);
|
||||
});
|
||||
}
|
||||
|
||||
function drawInitialMap() {
|
||||
recenterMap(game.mapCenter);
|
||||
drawControlPoints();
|
||||
drawGroundObjects();
|
||||
drawSupplyRoutes();
|
||||
drawFlightPlans();
|
||||
}
|
||||
|
||||
function clearAllLayers() {
|
||||
map.eachLayer(function (layer) {
|
||||
if (layer.clearLayers !== undefined) {
|
||||
layer.clearLayers();
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user