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 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]** 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.
|
* **[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.
|
* **[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]** 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.
|
* **[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):
|
def __init__(self):
|
||||||
self.controlpoints: List[ControlPoint] = []
|
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])
|
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||||
for x in self.landmap[1]:
|
for x in self.landmap[1]:
|
||||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
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):
|
def add_controlpoint(self, point: ControlPoint):
|
||||||
self.controlpoints.append(point)
|
self.controlpoints.append(point)
|
||||||
|
|
||||||
@ -637,35 +661,6 @@ class ConflictTheater:
|
|||||||
return i
|
return i
|
||||||
raise KeyError(f"Cannot find ControlPoint with ID {id}")
|
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
|
@staticmethod
|
||||||
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
||||||
theaters = {
|
theaters = {
|
||||||
@ -694,15 +689,11 @@ class ConflictTheater:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def point_to_ll(self, point: Point) -> LatLon:
|
def point_to_ll(self, point: Point) -> LatLon:
|
||||||
lat, lon = Transformer.from_crs(
|
lat, lon = self.point_to_ll_transformer.transform(point.x, point.y)
|
||||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
|
||||||
).transform(point.x, point.y)
|
|
||||||
return LatLon(lat, lon)
|
return LatLon(lat, lon)
|
||||||
|
|
||||||
def ll_to_point(self, ll: LatLon) -> Point:
|
def ll_to_point(self, ll: LatLon) -> Point:
|
||||||
x, y = Transformer.from_crs(
|
x, y = self.ll_to_point_transformer.transform(ll.latitude, ll.longitude)
|
||||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
|
||||||
).transform(ll.latitude, ll.longitude)
|
|
||||||
return Point(x, y)
|
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
|
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ def run_ui(game: Optional[Game] = None) -> None:
|
|||||||
GameUpdateSignal()
|
GameUpdateSignal()
|
||||||
|
|
||||||
# Start window
|
# Start window
|
||||||
window = QLiberationWindow(game)
|
window = QLiberationWindow(game, new_map)
|
||||||
window.showMaximized()
|
window.showMaximized()
|
||||||
splash.finish(window)
|
splash.finish(window)
|
||||||
qt_execution_code = app.exec_()
|
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.",
|
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 = subparsers.add_parser("new-game")
|
||||||
|
|
||||||
new_game.add_argument(
|
new_game.add_argument(
|
||||||
@ -239,7 +243,7 @@ def main():
|
|||||||
args.cheats,
|
args.cheats,
|
||||||
)
|
)
|
||||||
|
|
||||||
run_ui(game)
|
run_ui(game, args.new_map)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -4,7 +4,15 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from functools import singledispatchmethod
|
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 import QtCore, QtWidgets
|
||||||
from PySide2.QtCore import QLineF, QPointF, QRectF, Qt
|
from PySide2.QtCore import QLineF, QPointF, QRectF, Qt
|
||||||
@ -17,6 +25,11 @@ from PySide2.QtGui import (
|
|||||||
QPolygonF,
|
QPolygonF,
|
||||||
QWheelEvent,
|
QWheelEvent,
|
||||||
)
|
)
|
||||||
|
from PySide2.QtWebChannel import QWebChannel
|
||||||
|
from PySide2.QtWebEngineWidgets import (
|
||||||
|
QWebEnginePage,
|
||||||
|
QWebEngineView,
|
||||||
|
)
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QFrame,
|
QFrame,
|
||||||
QGraphicsItem,
|
QGraphicsItem,
|
||||||
@ -40,7 +53,10 @@ import qt_ui.uiconstants as CONST
|
|||||||
from game import Game
|
from game import Game
|
||||||
from game.navmesh import NavMesh
|
from game.navmesh import NavMesh
|
||||||
from game.theater import ControlPoint, Enum
|
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 (
|
from game.theater.theatergroundobject import (
|
||||||
TheaterGroundObject,
|
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.QMapGroundObject import QMapGroundObject
|
||||||
from qt_ui.widgets.map.ShippingLaneSegment import ShippingLaneSegment
|
from qt_ui.widgets.map.ShippingLaneSegment import ShippingLaneSegment
|
||||||
from qt_ui.widgets.map.SupplyRouteSegment import SupplyRouteSegment
|
from qt_ui.widgets.map.SupplyRouteSegment import SupplyRouteSegment
|
||||||
|
from qt_ui.widgets.map.mapmodel import MapModel
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
|
|
||||||
MAX_SHIP_DISTANCE = nautical_miles(80)
|
MAX_SHIP_DISTANCE = nautical_miles(80)
|
||||||
@ -112,17 +129,65 @@ class QLiberationMapState(Enum):
|
|||||||
MOVING_UNIT = 1
|
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
|
WAYPOINT_SIZE = 4
|
||||||
reference_point_setup_mode = False
|
reference_point_setup_mode = False
|
||||||
instance: Optional[QLiberationMap] = None
|
instance: Optional[QLiberationMap] = None
|
||||||
|
|
||||||
def __init__(self, game_model: GameModel):
|
def __init__(self, game_model: GameModel) -> None:
|
||||||
super(QLiberationMap, self).__init__()
|
super().__init__()
|
||||||
QLiberationMap.instance = self
|
QLiberationMap.instance = self
|
||||||
self.game_model = game_model
|
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.state = QLiberationMapState.NORMAL
|
||||||
|
|
||||||
self.waypoint_info_font = QFont()
|
self.waypoint_info_font = QFont()
|
||||||
@ -138,7 +203,7 @@ class QLiberationMap(QGraphicsView):
|
|||||||
self.factor = 1
|
self.factor = 1
|
||||||
self.factorized = 1
|
self.factorized = 1
|
||||||
self.init_scene()
|
self.init_scene()
|
||||||
self.setGame(game_model.game)
|
self.set_game(game_model.game)
|
||||||
|
|
||||||
# Object displayed when unit is selected
|
# Object displayed when unit is selected
|
||||||
self.movement_line = QtWidgets.QGraphicsLineItem(
|
self.movement_line = QtWidgets.QGraphicsLineItem(
|
||||||
@ -201,7 +266,7 @@ class QLiberationMap(QGraphicsView):
|
|||||||
self.setFrameShape(QFrame.NoFrame)
|
self.setFrameShape(QFrame.NoFrame)
|
||||||
self.setDragMode(QGraphicsView.ScrollHandDrag)
|
self.setDragMode(QGraphicsView.ScrollHandDrag)
|
||||||
|
|
||||||
def setGame(self, game: Optional[Game]):
|
def set_game(self, game: Optional[Game]):
|
||||||
should_recenter = self.game is None
|
should_recenter = self.game is None
|
||||||
self.game = game
|
self.game = game
|
||||||
if self.game is not None:
|
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)
|
budgetupdated = Signal(Game)
|
||||||
debriefingReceived = Signal(Debriefing)
|
debriefingReceived = Signal(Debriefing)
|
||||||
|
|
||||||
|
game_loaded = Signal(Game)
|
||||||
|
|
||||||
flight_paths_changed = Signal()
|
flight_paths_changed = Signal()
|
||||||
package_selection_changed = Signal(int) # -1 indicates no selection.
|
package_selection_changed = Signal(int) # -1 indicates no selection.
|
||||||
flight_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__()
|
super(GameUpdateSignal, self).__init__()
|
||||||
GameUpdateSignal.instance = self
|
GameUpdateSignal.instance = self
|
||||||
|
|
||||||
|
self.game_loaded.connect(self.updateGame)
|
||||||
|
|
||||||
def select_package(self, index: Optional[int]) -> None:
|
def select_package(self, index: Optional[int]) -> None:
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
self.package_selection_changed.emit(-1 if index is None else index)
|
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.uiconstants import URLS
|
||||||
from qt_ui.widgets.QTopPanel import QTopPanel
|
from qt_ui.widgets.QTopPanel import QTopPanel
|
||||||
from qt_ui.widgets.ato import QAirTaskingOrderPanel
|
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.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
|
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
|
||||||
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
|
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
|
||||||
@ -38,7 +38,7 @@ from qt_ui.windows.preferences.QLiberationPreferencesWindow import (
|
|||||||
|
|
||||||
|
|
||||||
class QLiberationWindow(QMainWindow):
|
class QLiberationWindow(QMainWindow):
|
||||||
def __init__(self, game: Optional[Game]) -> None:
|
def __init__(self, game: Optional[Game], new_map: bool) -> None:
|
||||||
super(QLiberationWindow, self).__init__()
|
super(QLiberationWindow, self).__init__()
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
@ -46,7 +46,7 @@ class QLiberationWindow(QMainWindow):
|
|||||||
Dialog.set_game(self.game_model)
|
Dialog.set_game(self.game_model)
|
||||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||||
self.info_panel = QInfoPanel(self.game)
|
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.setGeometry(300, 100, 270, 100)
|
||||||
self.setWindowTitle(f"DCS Liberation - v{VERSION}")
|
self.setWindowTitle(f"DCS Liberation - v{VERSION}")
|
||||||
@ -254,14 +254,13 @@ class QLiberationWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
if file is not None:
|
if file is not None:
|
||||||
game = persistency.load_game(file[0])
|
game = persistency.load_game(file[0])
|
||||||
GameUpdateSignal.get_instance().updateGame(game)
|
GameUpdateSignal.get_instance().game_loaded.emit(game)
|
||||||
|
|
||||||
def saveGame(self):
|
def saveGame(self):
|
||||||
logging.info("Saving game")
|
logging.info("Saving game")
|
||||||
|
|
||||||
if self.game.savepath:
|
if self.game.savepath:
|
||||||
persistency.save_game(self.game)
|
persistency.save_game(self.game)
|
||||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
|
||||||
liberation_install.setup_last_save_file(self.game.savepath)
|
liberation_install.setup_last_save_file(self.game.savepath)
|
||||||
liberation_install.save_config()
|
liberation_install.save_config()
|
||||||
else:
|
else:
|
||||||
@ -283,7 +282,12 @@ class QLiberationWindow(QMainWindow):
|
|||||||
def onGameGenerated(self, game: Game):
|
def onGameGenerated(self, game: Game):
|
||||||
logging.info("On Game generated")
|
logging.info("On Game generated")
|
||||||
self.game = game
|
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]):
|
def setGame(self, game: Optional[Game]):
|
||||||
try:
|
try:
|
||||||
@ -291,8 +295,7 @@ class QLiberationWindow(QMainWindow):
|
|||||||
if self.info_panel is not None:
|
if self.info_panel is not None:
|
||||||
self.info_panel.setGame(game)
|
self.info_panel.setGame(game)
|
||||||
self.game_model.set(self.game)
|
self.game_model.set(self.game)
|
||||||
if self.liberation_map is not None:
|
self.liberation_map.set_game(game)
|
||||||
self.liberation_map.setGame(game)
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logging.exception("Incompatible save game")
|
logging.exception("Incompatible save game")
|
||||||
QMessageBox.critical(
|
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
|
from gen.flights.flight import FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
class QFlightWaypointInfoBox(QGroupBox):
|
class QFlightWaypointInfoBox(QGroupBox):
|
||||||
def __init__(self, flight_wpt: FlightWaypoint = None):
|
def __init__(self) -> None:
|
||||||
super(QFlightWaypointInfoBox, self).__init__("Waypoint")
|
super(QFlightWaypointInfoBox, self).__init__("Waypoint")
|
||||||
self.flight_wpt = flight_wpt
|
self.x_position_label = QLabel("0")
|
||||||
if flight_wpt is None:
|
self.y_position_label = QLabel("0")
|
||||||
self.flight_wpt = FlightWaypoint(0, 0, 0)
|
self.alt_label = QLabel("0")
|
||||||
self.x_position_label = QLabel(str(self.flight_wpt.x))
|
self.name_label = QLabel("")
|
||||||
self.y_position_label = QLabel(str(self.flight_wpt.y))
|
self.desc_label = QLabel("")
|
||||||
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.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self) -> None:
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
@ -53,13 +50,10 @@ class QFlightWaypointInfoBox(QGroupBox):
|
|||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def set_flight_waypoint(self, flight_wpt: FlightWaypoint):
|
def set_flight_waypoint(self, flight_wpt: FlightWaypoint) -> None:
|
||||||
self.flight_wpt = flight_wpt
|
self.x_position_label.setText(str(flight_wpt.x))
|
||||||
if flight_wpt is None:
|
self.y_position_label.setText(str(flight_wpt.y))
|
||||||
self.flight_wpt = FlightWaypoint(0, 0, 0)
|
self.alt_label.setText(str(int(flight_wpt.alt.feet)))
|
||||||
self.x_position_label.setText(str(self.flight_wpt.x))
|
self.name_label.setText(str(flight_wpt.name))
|
||||||
self.y_position_label.setText(str(self.flight_wpt.y))
|
self.desc_label.setText(str(flight_wpt.description))
|
||||||
self.alt_label.setText(str(int(self.flight_wpt.alt.feet)))
|
self.setTitle(flight_wpt.name)
|
||||||
self.name_label.setText(str(self.flight_wpt.name))
|
|
||||||
self.desc_label.setText(str(self.flight_wpt.description))
|
|
||||||
self.setTitle(self.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