diff --git a/game/missiongenerator/kneeboard.py b/game/missiongenerator/kneeboard.py index 93e81c57..a1bc0c6f 100644 --- a/game/missiongenerator/kneeboard.py +++ b/game/missiongenerator/kneeboard.py @@ -41,7 +41,7 @@ from game.data.alic import AlicCodes from game.dcs.aircrafttype import AircraftType from game.radio.radios import RadioFrequency from game.runways import RunwayData -from game.theater import ConflictTheater, LatLon, TheaterGroundObject, TheaterUnit +from game.theater import TheaterGroundObject, TheaterUnit from game.theater.bullseye import Bullseye from game.utils import Distance, UnitSystem, meters, mps, pounds from game.weather import Weather @@ -183,12 +183,6 @@ class KneeboardPage: """Writes the kneeboard page to the given path.""" raise NotImplementedError - @staticmethod - def format_ll(ll: LatLon) -> str: - ns = "N" if ll.lat >= 0 else "S" - ew = "E" if ll.lng >= 0 else "W" - return f"{ll.lat:.4}°{ns} {ll.lng:.4}°{ew}" - @dataclass(frozen=True) class NumberedWaypoint: @@ -325,14 +319,12 @@ class BriefingPage(KneeboardPage): self, flight: FlightData, bullseye: Bullseye, - theater: ConflictTheater, weather: Weather, start_time: datetime.datetime, dark_kneeboard: bool, ) -> None: self.flight = flight self.bullseye = bullseye - self.theater = theater self.weather = weather self.start_time = start_time self.dark_kneeboard = dark_kneeboard @@ -399,7 +391,7 @@ class BriefingPage(KneeboardPage): font=self.flight_plan_font, ) - writer.text(f"Bullseye: {self.bullseye.to_lat_lon(self.theater).format_dms()}") + writer.text(f"Bullseye: {self.bullseye.position.latlng().format_dms()}") qnh_in_hg = f"{self.weather.atmospheric.qnh.inches_hg:.2f}" qnh_mm_hg = f"{self.weather.atmospheric.qnh.mm_hg:.1f}" @@ -598,12 +590,9 @@ class SupportPage(KneeboardPage): class SeadTaskPage(KneeboardPage): """A kneeboard page containing SEAD/DEAD target information.""" - def __init__( - self, flight: FlightData, dark_kneeboard: bool, theater: ConflictTheater - ) -> None: + def __init__(self, flight: FlightData, dark_kneeboard: bool) -> None: self.flight = flight self.dark_kneeboard = dark_kneeboard - self.theater = theater @property def target_units(self) -> Iterator[TheaterUnit]: @@ -634,7 +623,7 @@ class SeadTaskPage(KneeboardPage): writer.write(path) def target_info_row(self, unit: TheaterUnit) -> List[str]: - ll = self.theater.point_to_ll(unit.position) + ll = unit.position.latlng() unit_type = unit.type name = unit.name if unit_type is None else unit_type.name return [ @@ -647,15 +636,9 @@ class SeadTaskPage(KneeboardPage): class StrikeTaskPage(KneeboardPage): """A kneeboard page containing strike target information.""" - def __init__( - self, - flight: FlightData, - dark_kneeboard: bool, - theater: ConflictTheater, - ) -> None: + def __init__(self, flight: FlightData, dark_kneeboard: bool) -> None: self.flight = flight self.dark_kneeboard = dark_kneeboard - self.theater = theater @property def targets(self) -> Iterator[NumberedWaypoint]: @@ -678,12 +661,12 @@ class StrikeTaskPage(KneeboardPage): writer.write(path) - def target_info_row(self, target: NumberedWaypoint) -> List[str]: - ll = self.theater.point_to_ll(target.waypoint.position) + @staticmethod + def target_info_row(target: NumberedWaypoint) -> list[str]: return [ str(target.number), target.waypoint.pretty_name, - ll.format_dms(include_decimal_seconds=True), + target.waypoint.position.latlng().format_dms(include_decimal_seconds=True), ] @@ -748,9 +731,9 @@ class KneeboardGenerator(MissionInfoGenerator): def generate_task_page(self, flight: FlightData) -> Optional[KneeboardPage]: if flight.flight_type in (FlightType.DEAD, FlightType.SEAD): - return SeadTaskPage(flight, self.dark_kneeboard, self.game.theater) + return SeadTaskPage(flight, self.dark_kneeboard) elif flight.flight_type is FlightType.STRIKE: - return StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater) + return StrikeTaskPage(flight, self.dark_kneeboard) return None def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: @@ -767,7 +750,6 @@ class KneeboardGenerator(MissionInfoGenerator): BriefingPage( flight, self.game.coalition_for(flight.friendly).bullseye, - self.game.theater, self.game.conditions.weather, zoned_time, self.dark_kneeboard, diff --git a/game/server/combat/models.py b/game/server/combat/models.py index 716cb6fa..215b08b1 100644 --- a/game/server/combat/models.py +++ b/game/server/combat/models.py @@ -2,9 +2,10 @@ from __future__ import annotations from uuid import UUID +from dcs.mapping import LatLng from pydantic import BaseModel -from game.server.leaflet import LeafletLatLon, LeafletPoly, ShapelyUtil +from game.server.leaflet import LeafletPoly, ShapelyUtil from game.sim.combat import FrozenCombat from game.sim.combat.aircombat import AirCombat from game.sim.combat.atip import AtIp @@ -14,8 +15,8 @@ from game.theater import ConflictTheater class FrozenCombatJs(BaseModel): id: UUID - flight_position: LeafletLatLon | None - target_positions: list[LeafletLatLon] | None + flight_position: LatLng | None + target_positions: list[LatLng] | None footprint: list[LeafletPoly] | None @staticmethod @@ -30,20 +31,15 @@ class FrozenCombatJs(BaseModel): if isinstance(combat, AtIp): return FrozenCombatJs( id=combat.id, - flight_position=theater.point_to_ll(combat.flight.position()).as_list(), - target_positions=[ - theater.point_to_ll(combat.flight.package.target.position).as_list() - ], + flight_position=combat.flight.position().latlng(), + target_positions=[combat.flight.package.target.position.latlng()], footprint=None, ) if isinstance(combat, DefendingSam): return FrozenCombatJs( id=combat.id, - flight_position=theater.point_to_ll(combat.flight.position()).as_list(), - target_positions=[ - theater.point_to_ll(sam.position).as_list() - for sam in combat.air_defenses - ], + flight_position=combat.flight.position().latlng(), + target_positions=[sam.position.latlng() for sam in combat.air_defenses], footprint=None, ) raise NotImplementedError(f"Unhandled FrozenCombat type: {combat.__class__}") diff --git a/game/server/eventstream/models.py b/game/server/eventstream/models.py index 3c843282..a083a1c9 100644 --- a/game/server/eventstream/models.py +++ b/game/server/eventstream/models.py @@ -22,8 +22,7 @@ class GameUpdateEventsJs(BaseModel): def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs: return GameUpdateEventsJs( updated_flights={ - f[0].id: game.theater.point_to_ll(f[1]).as_list() - for f in events.updated_flights + f[0].id: f[1].latlng().as_list() for f in events.updated_flights }, new_combats=[ FrozenCombatJs.for_combat(c, game.theater) for c in events.new_combats diff --git a/game/server/waypoints/models.py b/game/server/waypoints/models.py index a959bf40..ac1a1194 100644 --- a/game/server/waypoints/models.py +++ b/game/server/waypoints/models.py @@ -1,16 +1,16 @@ from __future__ import annotations +from dcs.mapping import LatLng from pydantic.dataclasses import dataclass from game.ato import FlightWaypoint from game.ato.flightwaypointtype import FlightWaypointType -from game.theater import ConflictTheater, LatLon @dataclass class FlightWaypointJs: name: str - position: LatLon + position: LatLng altitude_ft: float altitude_reference: str is_movable: bool @@ -18,9 +18,7 @@ class FlightWaypointJs: include_in_path: bool @staticmethod - def for_waypoint( - waypoint: FlightWaypoint, theater: ConflictTheater - ) -> FlightWaypointJs: + def for_waypoint(waypoint: FlightWaypoint) -> FlightWaypointJs: # Target *points* are the exact location of a unit, whereas the target area is # only the center of the objective. Allow moving the latter since its exact # location isn't very important. @@ -64,7 +62,7 @@ class FlightWaypointJs: return FlightWaypointJs( name=waypoint.name, - position=theater.point_to_ll(waypoint.position), + position=waypoint.position.latlng(), altitude_ft=waypoint.alt.feet, altitude_reference=waypoint.alt_type, is_movable=is_movable, diff --git a/game/server/waypoints/routes.py b/game/server/waypoints/routes.py index a24e5b75..51128eb7 100644 --- a/game/server/waypoints/routes.py +++ b/game/server/waypoints/routes.py @@ -1,7 +1,7 @@ from datetime import timedelta from uuid import UUID -from dcs.mapping import LatLng +from dcs.mapping import LatLng, Point from fastapi import APIRouter, Depends, HTTPException, status from game import Game @@ -26,12 +26,10 @@ def all_waypoints_for_flight( flight.departure.position, meters(0), "RADIO", - ), - game.theater, + ) ) return [departure] + [ - FlightWaypointJs.for_waypoint(w, game.theater) - for w in flight.flight_plan.waypoints + FlightWaypointJs.for_waypoint(w) for w in flight.flight_plan.waypoints ] @@ -47,7 +45,7 @@ def set_position( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) waypoint = flight.flight_plan.waypoints[waypoint_idx - 1] - waypoint.position = game.theater.ll_to_point(position) + waypoint.position = Point.from_latlng(position, game.theater.terrain) package_model = ( GameContext.get_model() .ato_model_for(flight.blue) diff --git a/game/theater/bullseye.py b/game/theater/bullseye.py index 1f3691c6..2c1aeaf6 100644 --- a/game/theater/bullseye.py +++ b/game/theater/bullseye.py @@ -1,15 +1,10 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict, TYPE_CHECKING +from typing import Dict from dcs import Point -from .latlon import LatLon - -if TYPE_CHECKING: - from .conflicttheater import ConflictTheater - @dataclass class Bullseye: @@ -17,6 +12,3 @@ class Bullseye: def to_pydcs(self) -> Dict[str, float]: return {"x": self.position.x, "y": self.position.y} - - def to_lat_lon(self, theater: ConflictTheater) -> LatLon: - return theater.point_to_ll(self.position) diff --git a/game/theater/caucasus.py b/game/theater/caucasus.py deleted file mode 100644 index 6b1cb67e..00000000 --- a/game/theater/caucasus.py +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT: -# This file is generated by resources/tools/export_coordinates.py. -from game.theater.projections import TransverseMercator - -PARAMETERS = TransverseMercator( - central_meridian=33, - false_easting=-99516.9999999732, - false_northing=-4998114.999999984, - scale_factor=0.9996, -) diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 6e2e01e8..240bc38f 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -4,9 +4,9 @@ import datetime import math from dataclasses import dataclass from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple +from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple -from dcs.mapping import LatLng, Point +from dcs.mapping import Point from dcs.terrain import ( caucasus, marianaislands, @@ -17,13 +17,10 @@ from dcs.terrain import ( thechannel, ) from dcs.terrain.terrain import Terrain -from pyproj import CRS, Transformer from shapely import geometry, ops from .frontline import FrontLine from .landmap import Landmap, load_landmap, poly_contains -from .latlon import LatLon -from .projections import TransverseMercator from .seasonalconditions import SeasonalConditions from ..utils import Heading @@ -50,36 +47,12 @@ class ConflictTheater: def __init__(self) -> None: 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) -> None: self.controlpoints.append(point) @@ -253,17 +226,6 @@ class ConflictTheater: def seasonal_conditions(self) -> SeasonalConditions: raise NotImplementedError - @property - def projection_parameters(self) -> TransverseMercator: - raise NotImplementedError - - def point_to_ll(self, point: Point) -> LatLon: - lat, lon = self.point_to_ll_transformer.transform(point.x, point.y) - return LatLon(lat, lon) - - def ll_to_point(self, ll: LatLng) -> Point: - return Point.from_latlng(ll, self.terrain) - def heading_to_conflict_from(self, position: Point) -> Optional[Heading]: # Heading for a Group to the enemy. # Should be the point between the nearest and the most distant conflict @@ -309,12 +271,6 @@ class CaucasusTheater(ConflictTheater): return CONDITIONS - @property - def projection_parameters(self) -> TransverseMercator: - from .caucasus import PARAMETERS - - return PARAMETERS - class PersianGulfTheater(ConflictTheater): terrain = persiangulf.PersianGulf() @@ -337,12 +293,6 @@ class PersianGulfTheater(ConflictTheater): return CONDITIONS - @property - def projection_parameters(self) -> TransverseMercator: - from .persiangulf import PARAMETERS - - return PARAMETERS - class NevadaTheater(ConflictTheater): terrain = nevada.Nevada() @@ -365,12 +315,6 @@ class NevadaTheater(ConflictTheater): return CONDITIONS - @property - def projection_parameters(self) -> TransverseMercator: - from .nevada import PARAMETERS - - return PARAMETERS - class NormandyTheater(ConflictTheater): terrain = normandy.Normandy() @@ -393,12 +337,6 @@ class NormandyTheater(ConflictTheater): return CONDITIONS - @property - def projection_parameters(self) -> TransverseMercator: - from .normandy import PARAMETERS - - return PARAMETERS - class TheChannelTheater(ConflictTheater): terrain = thechannel.TheChannel() @@ -421,12 +359,6 @@ class TheChannelTheater(ConflictTheater): return CONDITIONS - @property - def projection_parameters(self) -> TransverseMercator: - from .thechannel import PARAMETERS - - return PARAMETERS - class SyriaTheater(ConflictTheater): terrain = syria.Syria() @@ -449,12 +381,6 @@ class SyriaTheater(ConflictTheater): return CONDITIONS - @property - def projection_parameters(self) -> TransverseMercator: - from .syria import PARAMETERS - - return PARAMETERS - class MarianaIslandsTheater(ConflictTheater): terrain = marianaislands.MarianaIslands() @@ -477,9 +403,3 @@ class MarianaIslandsTheater(ConflictTheater): from .seasonalconditions.marianaislands import CONDITIONS return CONDITIONS - - @property - def projection_parameters(self) -> TransverseMercator: - from .marianaislands import PARAMETERS - - return PARAMETERS diff --git a/game/theater/latlon.py b/game/theater/latlon.py deleted file mode 100644 index 6d440638..00000000 --- a/game/theater/latlon.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import List, Tuple - -from pydantic.dataclasses import dataclass - - -@dataclass(frozen=True) -class LatLon: - # These names match Leaflet for easier interop. - lat: float - lng: float - - def as_list(self) -> List[float]: - return [self.lat, self.lng] - - @staticmethod - def _components(dimension: float) -> Tuple[int, int, float]: - degrees = int(dimension) - minutes = int(dimension * 60 % 60) - seconds = dimension * 3600 % 60 - return degrees, minutes, seconds - - def _format_component( - self, dimension: float, hemispheres: Tuple[str, str], seconds_precision: int - ) -> str: - hemisphere = hemispheres[0] if dimension >= 0 else hemispheres[1] - degrees, minutes, seconds = self._components(dimension) - return f"{degrees}°{minutes:02}'{seconds:02.{seconds_precision}f}\"{hemisphere}" - - def format_dms(self, include_decimal_seconds: bool = False) -> str: - precision = 2 if include_decimal_seconds else 0 - return " ".join( - [ - self._format_component(self.lat, ("N", "S"), precision), - self._format_component(self.lng, ("E", "W"), precision), - ] - ) diff --git a/game/theater/marianaislands.py b/game/theater/marianaislands.py deleted file mode 100644 index 3fc39672..00000000 --- a/game/theater/marianaislands.py +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT: -# This file is generated by resources/tools/export_coordinates.py. -from game.theater.projections import TransverseMercator - -PARAMETERS = TransverseMercator( - central_meridian=147, - false_easting=238417.99999989968, - false_northing=-1491840.000000048, - scale_factor=0.9996, -) diff --git a/game/theater/nevada.py b/game/theater/nevada.py deleted file mode 100644 index e18700d6..00000000 --- a/game/theater/nevada.py +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT: -# This file is generated by resources/tools/export_coordinates.py. -from game.theater.projections import TransverseMercator - -PARAMETERS = TransverseMercator( - central_meridian=-117, - false_easting=-193996.80999964548, - false_northing=-4410028.063999966, - scale_factor=0.9996, -) diff --git a/game/theater/normandy.py b/game/theater/normandy.py deleted file mode 100644 index 8f91ab78..00000000 --- a/game/theater/normandy.py +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT: -# This file is generated by resources/tools/export_coordinates.py. -from game.theater.projections import TransverseMercator - -PARAMETERS = TransverseMercator( - central_meridian=-3, - false_easting=-195526.00000000204, - false_northing=-5484812.999999951, - scale_factor=0.9996, -) diff --git a/game/theater/persiangulf.py b/game/theater/persiangulf.py deleted file mode 100644 index 69ce5288..00000000 --- a/game/theater/persiangulf.py +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT: -# This file is generated by resources/tools/export_coordinates.py. -from game.theater.projections import TransverseMercator - -PARAMETERS = TransverseMercator( - central_meridian=57, - false_easting=75755.99999999645, - false_northing=-2894933.0000000377, - scale_factor=0.9996, -) diff --git a/game/theater/projections.py b/game/theater/projections.py deleted file mode 100644 index 90f24fe2..00000000 --- a/game/theater/projections.py +++ /dev/null @@ -1,31 +0,0 @@ -from dataclasses import dataclass - -from pyproj import CRS - - -@dataclass(frozen=True) -class TransverseMercator: - central_meridian: int - false_easting: float - false_northing: float - scale_factor: float - - def to_crs(self) -> CRS: - return CRS.from_proj4( - " ".join( - [ - "+proj=tmerc", - "+lat_0=0", - f"+lon_0={self.central_meridian}", - f"+k_0={self.scale_factor}", - f"+x_0={self.false_easting}", - f"+y_0={self.false_northing}", - "+towgs84=0,0,0,0,0,0,0", - "+units=m", - "+vunits=m", - "+ellps=WGS84", - "+no_defs", - "+axis=neu", - ] - ) - ) diff --git a/game/theater/syria.py b/game/theater/syria.py deleted file mode 100644 index 6daff280..00000000 --- a/game/theater/syria.py +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT: -# This file is generated by resources/tools/export_coordinates.py. -from game.theater.projections import TransverseMercator - -PARAMETERS = TransverseMercator( - central_meridian=39, - false_easting=282801.00000003993, - false_northing=-3879865.9999999935, - scale_factor=0.9996, -) diff --git a/game/theater/thechannel.py b/game/theater/thechannel.py deleted file mode 100644 index 0ac20788..00000000 --- a/game/theater/thechannel.py +++ /dev/null @@ -1,10 +0,0 @@ -# DO NOT EDIT: -# This file is generated by resources/tools/export_coordinates.py. -from game.theater.projections import TransverseMercator - -PARAMETERS = TransverseMercator( - central_meridian=3, - false_easting=99376.00000000288, - false_northing=-5636889.00000001, - scale_factor=0.9996, -) diff --git a/qt_ui/widgets/map/model/controlpointjs.py b/qt_ui/widgets/map/model/controlpointjs.py index 4c05d541..7f505bc6 100644 --- a/qt_ui/widgets/map/model/controlpointjs.py +++ b/qt_ui/widgets/map/model/controlpointjs.py @@ -62,8 +62,7 @@ class ControlPointJs(QObject): @Property(list, notify=positionChanged) def position(self) -> LeafletLatLon: - ll = self.theater.point_to_ll(self.control_point.position) - return [ll.lat, ll.lng] + return self.control_point.position.latlng().as_list() @Property(bool, notify=mobileChanged) def mobile(self) -> bool: @@ -74,7 +73,7 @@ class ControlPointJs(QObject): if self.control_point.target_position is None: # Qt seems to convert None to [] for list Properties :( return [] - return self.theater.point_to_ll(self.control_point.target_position).as_list() + return self.control_point.target_position.latlng().as_list() def destination_in_range(self, destination: Point) -> bool: move_distance = meters( @@ -84,7 +83,9 @@ class ControlPointJs(QObject): @Slot(list, result=bool) def destinationInRange(self, destination: LeafletLatLon) -> bool: - return self.destination_in_range(self.theater.ll_to_point(LatLng(*destination))) + return self.destination_in_range( + Point.from_latlng(LatLng(*destination), self.theater.terrain) + ) @Slot(list, result=str) def setDestination(self, destination: LeafletLatLon) -> str: @@ -93,7 +94,7 @@ class ControlPointJs(QObject): if not self.control_point.captured: return f"{self.control_point} is not owned by player" - point = self.theater.ll_to_point(LatLng(*destination)) + point = Point.from_latlng(LatLng(*destination), self.theater.terrain) if not self.destination_in_range(point): return ( f"Cannot move {self.control_point} more than " diff --git a/qt_ui/widgets/map/model/flightjs.py b/qt_ui/widgets/map/model/flightjs.py index b73567c8..886293ca 100644 --- a/qt_ui/widgets/map/model/flightjs.py +++ b/qt_ui/widgets/map/model/flightjs.py @@ -5,7 +5,6 @@ from PySide2.QtCore import Property, QObject, Signal, Slot from game.ato import Flight from game.ato.flightstate import InFlight from game.server.leaflet import LeafletLatLon -from game.theater import ConflictTheater from qt_ui.models import AtoModel @@ -15,17 +14,10 @@ class FlightJs(QObject): blueChanged = Signal() selectedChanged = Signal() - def __init__( - self, - flight: Flight, - selected: bool, - theater: ConflictTheater, - ato_model: AtoModel, - ) -> None: + def __init__(self, flight: Flight, selected: bool, ato_model: AtoModel) -> None: super().__init__() self.flight = flight self._selected = selected - self.theater = theater self.ato_model = ato_model @Property(str, notify=idChanged) @@ -35,8 +27,7 @@ class FlightJs(QObject): @Property(list, notify=positionChanged) def position(self) -> LeafletLatLon: if isinstance(self.flight.state, InFlight): - ll = self.theater.point_to_ll(self.flight.state.estimate_position()) - return [ll.lat, ll.lng] + return self.flight.state.estimate_position().latlng().as_list() return [] @Property(bool, notify=blueChanged) diff --git a/qt_ui/widgets/map/model/frontlinejs.py b/qt_ui/widgets/map/model/frontlinejs.py index c448da49..3a5656ca 100644 --- a/qt_ui/widgets/map/model/frontlinejs.py +++ b/qt_ui/widgets/map/model/frontlinejs.py @@ -5,7 +5,7 @@ from typing import List from PySide2.QtCore import Property, QObject, Signal, Slot from game.server.leaflet import LeafletLatLon -from game.theater import ConflictTheater, FrontLine +from game.theater import FrontLine from game.utils import nautical_miles from qt_ui.dialogs import Dialog @@ -13,24 +13,19 @@ from qt_ui.dialogs import Dialog class FrontLineJs(QObject): extentsChanged = Signal() - def __init__(self, front_line: FrontLine, theater: ConflictTheater) -> None: + def __init__(self, front_line: FrontLine) -> None: super().__init__() self.front_line = front_line - self.theater = theater @Property(list, notify=extentsChanged) def extents(self) -> List[LeafletLatLon]: - a = self.theater.point_to_ll( - self.front_line.position.point_from_heading( - self.front_line.attack_heading.right.degrees, nautical_miles(2).meters - ) - ) - b = self.theater.point_to_ll( - self.front_line.position.point_from_heading( - self.front_line.attack_heading.left.degrees, nautical_miles(2).meters - ) - ) - return [[a.lat, a.lng], [b.lat, b.lng]] + a = self.front_line.position.point_from_heading( + self.front_line.attack_heading.right.degrees, nautical_miles(2).meters + ).latlng() + b = self.front_line.position.point_from_heading( + self.front_line.attack_heading.left.degrees, nautical_miles(2).meters + ).latlng() + return [a.as_list(), b.as_list()] @Slot() def showPackageDialog(self) -> None: diff --git a/qt_ui/widgets/map/model/groundobjectjs.py b/qt_ui/widgets/map/model/groundobjectjs.py index 7377567e..8cb3ba91 100644 --- a/qt_ui/widgets/map/model/groundobjectjs.py +++ b/qt_ui/widgets/map/model/groundobjectjs.py @@ -66,8 +66,7 @@ class GroundObjectJs(QObject): @Property(list, notify=positionChanged) def position(self) -> LeafletLatLon: - ll = self.theater.point_to_ll(self.tgo.position) - return [ll.lat, ll.lng] + return self.tgo.position.latlng().as_list() @Property(bool, notify=deadChanged) def dead(self) -> bool: diff --git a/qt_ui/widgets/map/model/ipcombatjs.py b/qt_ui/widgets/map/model/ipcombatjs.py index 70a28dbe..11c98601 100644 --- a/qt_ui/widgets/map/model/ipcombatjs.py +++ b/qt_ui/widgets/map/model/ipcombatjs.py @@ -16,7 +16,6 @@ class IpCombatJs(QObject): self._flight = FlightJs( combat.flight, selected=False, - theater=game_model.game.theater, ato_model=game_model.ato_model_for(combat.flight.squadron.player), ) diff --git a/qt_ui/widgets/map/model/mapmodel.py b/qt_ui/widgets/map/model/mapmodel.py index f6020f25..bf936c70 100644 --- a/qt_ui/widgets/map/model/mapmodel.py +++ b/qt_ui/widgets/map/model/mapmodel.py @@ -4,7 +4,7 @@ import logging from typing import List, Optional, Tuple from PySide2.QtCore import Property, QObject, Signal -from dcs import Point +from dcs.mapping import LatLng from game import Game from game.ato.airtaaskingorder import AirTaskingOrder @@ -63,7 +63,7 @@ class MapModel(QObject): def __init__(self, game_model: GameModel) -> None: super().__init__() self.game_model = game_model - self._map_center = [0, 0] + self._map_center = LatLng(0, 0) self._control_points = [] self._ground_objects = [] self._supply_routes = [] @@ -157,11 +157,6 @@ class MapModel(QObject): flight.set_selected(True) self.selectedFlightChanged.emit(str(flight.flight.id)) - @staticmethod - def leaflet_coord_for(point: Point, theater: ConflictTheater) -> LeafletLatLon: - ll = theater.point_to_ll(point) - return [ll.lat, ll.lng] - def reset(self) -> None: if self.game_model.game is None: self.clear() @@ -182,9 +177,8 @@ class MapModel(QObject): 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.lat, ll.lng] - self.mapCenterChanged.emit(self._map_center) + self._map_center = theater.terrain.map_view_default.position.latlng() + self.mapCenterChanged.emit(self._map_center.as_list()) @Property(str, notify=apiKeyChanged) def apiKey(self) -> str: @@ -192,7 +186,7 @@ class MapModel(QObject): @Property(list, notify=mapCenterChanged) def mapCenter(self) -> LeafletLatLon: - return self._map_center + return self._map_center.as_list() def _flights_in_ato( self, ato: AirTaskingOrder, blue: bool @@ -203,7 +197,6 @@ class MapModel(QObject): flights[blue, p_idx, f_idx] = FlightJs( flight, selected=blue and (p_idx, f_idx) == self._selected_flight_index, - theater=self.game.theater, ato_model=self.game_model.ato_model_for(blue), ) return flights @@ -262,10 +255,7 @@ class MapModel(QObject): SupplyRouteJs( control_point, destination, - [ - self.leaflet_coord_for(p, self.game.theater) - for p in convoy_route - ], + [p.latlng().as_list() for p in convoy_route], sea_route=False, game=self.game, ) @@ -278,10 +268,7 @@ class MapModel(QObject): SupplyRouteJs( control_point, destination, - [ - self.leaflet_coord_for(p, self.game.theater) - for p in shipping_lane - ], + [p.latlng().as_list() for p in shipping_lane], sea_route=True, game=self.game, ) @@ -293,9 +280,7 @@ class MapModel(QObject): return self._supply_routes def reset_front_lines(self) -> None: - self._front_lines = [ - FrontLineJs(f, self.game.theater) for f in self.game.theater.conflicts() - ] + self._front_lines = [FrontLineJs(f) for f in self.game.theater.conflicts()] self.frontLinesChanged.emit() @Property(list, notify=frontLinesChanged) diff --git a/qt_ui/widgets/map/model/samcombatjs.py b/qt_ui/widgets/map/model/samcombatjs.py index 027f45e5..6437ad74 100644 --- a/qt_ui/widgets/map/model/samcombatjs.py +++ b/qt_ui/widgets/map/model/samcombatjs.py @@ -14,11 +14,9 @@ class SamCombatJs(QObject): super().__init__() assert game_model.game is not None self.combat = combat - self.theater = game_model.game.theater self._flight = FlightJs( combat.flight, selected=False, - theater=game_model.game.theater, ato_model=game_model.ato_model_for(combat.flight.squadron.player), ) self._air_defenses = [ diff --git a/qt_ui/widgets/map/model/unculledzonejs.py b/qt_ui/widgets/map/model/unculledzonejs.py index bd2c5bd6..a7a26455 100644 --- a/qt_ui/widgets/map/model/unculledzonejs.py +++ b/qt_ui/widgets/map/model/unculledzonejs.py @@ -28,7 +28,6 @@ class UnculledZone(QObject): @classmethod def each_from_game(cls, game: Game) -> Iterator[UnculledZone]: for zone in game.get_culling_zones(): - ll = game.theater.point_to_ll(zone) yield UnculledZone( - [ll.lat, ll.lng], game.settings.perf_culling_distance * 1000 + zone.latlng().as_list(), game.settings.perf_culling_distance * 1000 ) diff --git a/resources/tools/coord_export.lua b/resources/tools/coord_export.lua deleted file mode 100644 index c5542522..00000000 --- a/resources/tools/coord_export.lua +++ /dev/null @@ -1,38 +0,0 @@ -local function dump_coords() - local coordinates = {} - local bases = world.getAirbases() - for i = 1, #bases do - local base = bases[i] - point = Airbase.getPoint(base) - lat, lon, alt = coord.LOtoLL(point) - coordinates[Airbase.getName(base)] = { - ["point"] = point, - ["LL"] = { - ["lat"] = lat, - ["lon"] = lon, - ["alt"] = alt, - }, - } - end - - zero = { - ["x"] = 0, - ["y"] = 0, - ["z"] = 0, - } - lat, lon, alt = coord.LOtoLL(zero) - coordinates["zero"] = { - ["point"] = zero, - ["LL"] = { - ["lat"] = lat, - ["lon"] = lon, - ["alt"] = alt, - }, - } - - local fp = io.open(lfs.writedir() .. "\\coords.json", 'w') - fp:write(json:encode(coordinates)) - fp:close() -end - -dump_coords() \ No newline at end of file diff --git a/resources/tools/export_coordinates.py b/resources/tools/export_coordinates.py deleted file mode 100644 index 0328d75d..00000000 --- a/resources/tools/export_coordinates.py +++ /dev/null @@ -1,260 +0,0 @@ -"""Command line tool for exporting coordinates from DCS to derive projection data. - -DCS X/Z coordinates are meter-scale projections of a transverse mercator grid. The -projection has a few required parameters: - -1. Scale factor. Is 0.9996 for most regions: - https://proj.org/operations/projections/tmerc.html. -2. Central meridian of the projection. Easily guessed because there are only 60 UTM - zones and one of those is always used. -3. A false easting and northing (offsets from UTM's center point to DCS's). These aren't - easily guessed, but can be computed by using an offset of 0 and finding the error of - projecting the 0 point from DCS. - -This tool creates a mission that will dump the lat/lon and x/z coordinates of the 0/0 -point and also every airport in the given theater. The data for the zero point is used -to compute the false easting and northing for the map. The data for each airport is used -to test the projection for errors. - -The resulting data is exported to game/theater/.py as a TransverseMercator object. -""" -from __future__ import annotations - -import argparse -import json -import math -import sys -import textwrap -from contextlib import contextmanager -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Dict - -from dcs import Mission -from dcs.action import DoScriptFile -from dcs.terrain.caucasus import Caucasus -from dcs.terrain.nevada import Nevada -from dcs.terrain.normandy import Normandy -from dcs.terrain.persiangulf import PersianGulf -from dcs.terrain.syria import Syria -from dcs.terrain.terrain import Terrain -from dcs.terrain.thechannel import TheChannel -from dcs.terrain.marianaislands import MarianaIslands -from dcs.triggers import TriggerStart -from pyproj import CRS, Transformer - -from game import persistency -from game.theater.projections import TransverseMercator -from qt_ui import liberation_install - -THIS_DIR = Path(__file__).resolve().parent -JSON_LUA = THIS_DIR.parent / "plugins/base/json.lua" -EXPORT_LUA = THIS_DIR / "coord_export.lua" -SAVE_DIR = THIS_DIR.parent / "coordinate_reference" - - -ARG_TO_TERRAIN_MAP = { - "caucasus": Caucasus(), - "nevada": Nevada(), - "normandy": Normandy(), - "persiangulf": PersianGulf(), - "thechannel": TheChannel(), - "syria": Syria(), - "marianaislands": MarianaIslands(), -} - -# https://gisgeography.com/central-meridian/ -# UTM zones determined by guess and check. There are only a handful in the region for -# each map and getting the wrong one will be flagged with errors when processing. -CENTRAL_MERIDIANS = { - "caucasus": 33, - "nevada": -117, - "normandy": -3, - "persiangulf": 57, - "thechannel": 3, - "syria": 39, - "marianaislands": 147, -} - - -@dataclass(frozen=True) -class Coordinates: - x: float - y: float - z: float - - latitude: float - longitude: float - altitude: float - - @classmethod - def from_json(cls, data: Dict[str, Any]) -> Coordinates: - return cls( - x=data["point"]["x"], - y=data["point"]["y"], - z=data["point"]["z"], - latitude=data["LL"]["lat"], - longitude=data["LL"]["lon"], - altitude=data["LL"]["alt"], - ) - - -def create_mission(terrain: Terrain) -> Path: - m = Mission(terrain) - - json_trigger = TriggerStart(comment=f"Load JSON") - json_lua = m.map_resource.add_resource_file(JSON_LUA) - json_trigger.add_action(DoScriptFile(json_lua)) - m.triggerrules.triggers.append(json_trigger) - - export_trigger = TriggerStart(comment=f"Load coordinate export") - export_lua = m.map_resource.add_resource_file(EXPORT_LUA) - export_trigger.add_action(DoScriptFile(export_lua)) - m.triggerrules.triggers.append(export_trigger) - - mission_path = persistency.mission_path_for(f"export_{terrain.name.lower()}.miz") - m.save(mission_path) - return mission_path - - -def load_coordinate_data(data: Dict[str, Any]) -> Dict[str, Coordinates]: - airbases = {} - for name, coord_data in data.items(): - airbases[name] = Coordinates.from_json(coord_data) - return airbases - - -def test_for_errors( - name: str, - lat_lon_to_x_z: Transformer, - x_z_to_lat_lon: Transformer, - coords: Coordinates, -) -> bool: - errors = False - - x, z = lat_lon_to_x_z.transform(coords.latitude, coords.longitude) - if not math.isclose(x, coords.x) or not math.isclose(z, coords.z): - error_x = x - coords.x - error_z = z - coords.z - error_pct_x = error_x / coords.x * 100 - error_pct_z = error_z / coords.z * 100 - print(f"{name} has error of {error_pct_x}% {error_pct_z}%") - errors = True - - lat, lon = x_z_to_lat_lon.transform(coords.x, coords.z) - if not math.isclose(lat, coords.latitude) or not math.isclose( - lon, coords.longitude - ): - error_lat = lat - coords.latitude - error_lon = lon - coords.longitude - error_pct_lon = error_lat / coords.latitude * 100 - error_pct_lat = error_lon / coords.longitude * 100 - print(f"{name} has error of {error_pct_lat}% {error_pct_lon}%") - errors = True - - return errors - - -def test_parameters( - airbases: Dict[str, Coordinates], parameters: TransverseMercator -) -> bool: - errors = False - wgs84 = CRS("WGS84") - crs = parameters.to_crs() - lat_lon_to_x_z = Transformer.from_crs(wgs84, crs) - x_z_to_lat_lon = Transformer.from_crs(crs, wgs84) - for name, coords in airbases.items(): - if name == "zero": - continue - if test_for_errors(name, lat_lon_to_x_z, x_z_to_lat_lon, coords): - errors = True - return errors - - -def compute_tmerc_parameters( - coordinates_file: Path, terrain: str -) -> TransverseMercator: - - data = json.loads(coordinates_file.read_text()) - airbases = load_coordinate_data(data) - wgs84 = CRS("WGS84") - - # Creates a transformer with 0 for the false easting and northing, but otherwise has - # the correct parameters. We'll use this to transform the zero point from the - # mission to calculate the error from the actual zero point to determine the correct - # false easting and northing. - bad = TransverseMercator( - central_meridian=CENTRAL_MERIDIANS[terrain], - false_easting=0, - false_northing=0, - scale_factor=0.9996, - ).to_crs() - zero_finder = Transformer.from_crs(wgs84, bad) - z, x = zero_finder.transform(airbases["zero"].latitude, airbases["zero"].longitude) - - parameters = TransverseMercator( - central_meridian=CENTRAL_MERIDIANS[terrain], - false_easting=-x, - false_northing=-z, - scale_factor=0.9996, - ) - - if test_parameters(airbases, parameters): - sys.exit("Found errors in projection parameters. Quitting.") - - return parameters - - -@contextmanager -def mission_scripting(): - liberation_install.replace_mission_scripting_file() - try: - yield - finally: - liberation_install.restore_original_mission_scripting() - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - - parser.add_argument("map", choices=list(ARG_TO_TERRAIN_MAP.keys())) - - return parser.parse_args() - - -def main() -> None: - if liberation_install.init(): - print("Set up Liberation first.") - return - - args = parse_args() - terrain = ARG_TO_TERRAIN_MAP[args.map] - mission = create_mission(terrain) - with mission_scripting(): - input( - f"Created {mission} and replaced MissionScript.lua. Open DCS and load the " - "mission. Once the mission starts running, close it and press enter." - ) - coords_path = Path(persistency.base_path()) / "coords.json" - parameters = compute_tmerc_parameters(coords_path, args.map) - out_file = THIS_DIR.parent.parent / "game/theater" / f"{args.map}.py" - out_file.write_text( - textwrap.dedent( - f"""\ - # DO NOT EDIT: - # This file is generated by resources/tools/export_coordinates.py. - from game.theater.projections import TransverseMercator - - PARAMETERS = TransverseMercator( - central_meridian={parameters.central_meridian}, - false_easting={parameters.false_easting}, - false_northing={parameters.false_northing}, - scale_factor={parameters.scale_factor}, - ) - """ - ) - ) - - -if __name__ == "__main__": - main()