mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Remove our old lat/lon support code.
pydcs provides this now.
This commit is contained in:
parent
bb72acd3ac
commit
1a9930b93a
@ -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,
|
||||
|
||||
@ -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__}")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
]
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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",
|
||||
]
|
||||
)
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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 "
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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),
|
||||
)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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()
|
||||
@ -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()
|
||||
Loading…
x
Reference in New Issue
Block a user