Remove our old lat/lon support code.

pydcs provides this now.
This commit is contained in:
Dan Albert 2022-02-22 17:40:07 -08:00
parent bb72acd3ac
commit 1a9930b93a
26 changed files with 57 additions and 640 deletions

View File

@ -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,

View File

@ -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__}")

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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,
)

View File

@ -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

View File

@ -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),
]
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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",
]
)
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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 "

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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),
)

View File

@ -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)

View File

@ -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 = [

View File

@ -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
)

View File

@ -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()

View File

@ -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/<map>.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()