mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Add data for lat/lon conversions.
This commit is contained in:
parent
2b8dfc9dbc
commit
8a01209ded
8
game/theater/caucasus.py
Normal file
8
game/theater/caucasus.py
Normal file
@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=33,
|
||||
false_easting=-99516.9999999732,
|
||||
false_northing=-4998114.999999984,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
8
game/theater/nevada.py
Normal file
8
game/theater/nevada.py
Normal file
@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=-117,
|
||||
false_easting=-193996.80999964548,
|
||||
false_northing=-4410028.063999966,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
8
game/theater/normandy.py
Normal file
8
game/theater/normandy.py
Normal file
@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=-3,
|
||||
false_easting=-195526.00000000204,
|
||||
false_northing=-5484812.999999951,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
8
game/theater/persiangulf.py
Normal file
8
game/theater/persiangulf.py
Normal file
@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=57,
|
||||
false_easting=75755.99999999645,
|
||||
false_northing=-2894933.0000000377,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
31
game/theater/projections.py
Normal file
31
game/theater/projections.py
Normal file
@ -0,0 +1,31 @@
|
||||
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",
|
||||
]
|
||||
)
|
||||
)
|
||||
8
game/theater/syria.py
Normal file
8
game/theater/syria.py
Normal file
@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=39,
|
||||
false_easting=282801.00000003993,
|
||||
false_northing=-3879865.9999999935,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
8
game/theater/thechannel.py
Normal file
8
game/theater/thechannel.py
Normal file
@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=3,
|
||||
false_easting=99376.00000000288,
|
||||
false_northing=-5636889.00000001,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
@ -1,6 +1,7 @@
|
||||
altgraph==0.17
|
||||
appdirs==1.4.4
|
||||
black==21.4b0
|
||||
certifi==2020.12.5
|
||||
cfgv==3.2.0
|
||||
click==7.1.2
|
||||
distlib==0.3.1
|
||||
@ -17,6 +18,7 @@ pefile==2019.4.18
|
||||
Pillow==8.1.1
|
||||
pre-commit==2.10.1
|
||||
PyInstaller==3.6
|
||||
pyproj==3.0.1
|
||||
PySide2==5.15.2
|
||||
pywin32-ctypes==0.2.0
|
||||
PyYAML==5.4.1
|
||||
|
||||
38
resources/tools/coord_export.lua
Normal file
38
resources/tools/coord_export.lua
Normal file
@ -0,0 +1,38 @@
|
||||
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()
|
||||
255
resources/tools/export_coordinates.py
Normal file
255
resources/tools/export_coordinates.py
Normal file
@ -0,0 +1,255 @@
|
||||
"""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.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(),
|
||||
}
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
|
||||
@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 Path(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"""\
|
||||
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