Add data for lat/lon conversions.

This commit is contained in:
Dan Albert 2020-12-09 20:35:30 -08:00
parent 2b8dfc9dbc
commit 8a01209ded
10 changed files with 374 additions and 0 deletions

8
game/theater/caucasus.py Normal file
View 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
View 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
View 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,
)

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

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

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

View File

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

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

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