Skip to content

Commit 098bc04

Browse files
committed
CHANGES:
- FunkMan: proper dependency added - Mission: /convert reworked - Utils: dd_to_mgrs(), mgrs_to_dd() dd_to_dmm(), dms_to_dd() added, dd_to_dms() reworked - LotAtc: Windows 2016 server version check added.
1 parent 55bb33c commit 098bc04

File tree

9 files changed

+249
-84
lines changed

9 files changed

+249
-84
lines changed

core/utils/dcs.py

Lines changed: 177 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
import stat
88
import sys
99

10+
# import the correct mgrs library
11+
if sys.version_info < (3, 13):
12+
import mgrs
13+
else:
14+
from mgrs import LLtoUTM, encode, UTMtoLL, decode
15+
1016
from contextlib import suppress
1117
from core.const import SAVED_GAMES
1218
from core.data.node import Node
1319
from core.utils.helper import alternate_parse_settings
14-
from pathlib import Path
1520
from typing import Optional
1621

1722
__all__ = [
@@ -20,6 +25,10 @@
2025
"desanitize",
2126
"is_desanitized",
2227
"dd_to_dms",
28+
"dd_to_dmm",
29+
"dms_to_dd",
30+
"dd_to_mgrs",
31+
"mgrs_to_dd",
2332
"get_active_runways",
2433
"create_writable_mission",
2534
"get_orig_file",
@@ -133,11 +142,173 @@ def is_desanitized(node: Node) -> bool:
133142
return True
134143

135144

136-
def dd_to_dms(dd):
137-
frac, degrees = math.modf(dd)
138-
frac, minutes = math.modf(frac * 60)
139-
frac, seconds = math.modf(frac * 60)
140-
return degrees, minutes, seconds, frac
145+
def dd_to_dms(dd: float, precision: int = 3) -> tuple[int, int, int, int]:
146+
"""
147+
Convert a decimal‑degree value into a DMS tuple.
148+
149+
Parameters
150+
----------
151+
dd : float
152+
Latitude or longitude in decimal degrees.
153+
precision: int, optional
154+
Number of decimal places for the seconds fraction.
155+
The function returns the fraction as an *integer*:
156+
`precision = 2` → hundredths of a second (0‑99)
157+
`precision = 3` → milliseconds (0‑999) – default.
158+
159+
Returns
160+
-------
161+
tuple[int, int, int, int]
162+
(degrees, minutes, seconds, fraction)
163+
All four components are integers. The sign of the value is
164+
stored only in the *degrees* component.
165+
166+
Examples
167+
--------
168+
>>> dd_to_dms(49.001234)
169+
(49, 0, 0, 78) # 78 × 10⁻³ s = 0.078s
170+
>>> dd_to_dms(-49.999999, precision=2)
171+
(-49, 59, 59, 99) # -49°59'59.99″
172+
"""
173+
# --- 1️⃣ sign & absolute value ------------------------------------
174+
sign = 1 if dd >= 0 else -1
175+
dd_abs = abs(dd)
176+
177+
# --- 2️⃣ whole degrees & remaining fraction ------------------------
178+
deg, frac = divmod(dd_abs, 1) # deg → float, frac → 0‑1
179+
deg = int(deg) # int degrees
180+
181+
# --- 3️⃣ minutes ----------------------------------------------------
182+
minu, frac = divmod(frac * 60, 1) # minu → int, frac → 0‑1
183+
minu = int(minu)
184+
185+
# --- 4️⃣ seconds ---------------------------------------------------
186+
sec, frac = divmod(frac * 60, 1) # sec → int, frac → 0‑1
187+
sec = int(sec)
188+
189+
# --- 5️⃣ fractional seconds (as an integer) -----------------------
190+
frac_units = 10 ** precision # 100→2decimals, 1000→3decimals…
191+
frac_int = int(round(frac * frac_units))
192+
193+
# --- 6️⃣ carry‑over (rounding may produce 60′/60″) -----------------
194+
if frac_int >= frac_units: # e.g. 0.999… rounds to 1.0s
195+
sec += 1
196+
frac_int = 0
197+
198+
if sec >= 60:
199+
minu += 1
200+
sec = 0
201+
202+
if minu >= 60:
203+
deg += 1
204+
minu = 0
205+
206+
# --- 7️⃣ apply sign to the degrees component -----------------------
207+
deg *= sign
208+
209+
return deg, minu, sec, frac_int
210+
211+
212+
def dd_to_dmm(lat: float, lon: float, prec: int = 2) -> str:
213+
"""
214+
Convert a lat/lon pair (in decimal degrees) into a DMM string:
215+
216+
N 38°53.217' E 122°24.300'
217+
218+
"""
219+
eps = 1e-9 # tolerance for floating‑point rounding
220+
221+
def _fmt(val: float, is_lat: bool) -> str:
222+
sign = 1 if val >= 0 else -1
223+
dir_ = ('N' if is_lat else 'E') if sign == 1 else ('S' if is_lat else 'W')
224+
abs_val = abs(val)
225+
226+
deg = int(math.floor(abs_val))
227+
mins_raw = (abs_val - deg) * 60
228+
229+
# ---- Normalise minutes ----
230+
mins = round(mins_raw, prec) # round to desired precision first
231+
if mins >= 60 - eps: # minute overflow → carry to next degree
232+
deg += 1
233+
mins = 0.0
234+
235+
return f'{dir_} {deg}°{mins:0{prec+3}.{prec}f}\''
236+
# format: e.g. "E 10°00.00'"
237+
238+
return f'{_fmt(lat, True)} {_fmt(lon, False)}'
239+
240+
241+
_dms_re = re.compile(
242+
r"""^\s*
243+
(?P<dir>[NSEW])? # optional direction letter
244+
(?P<deg>\d{2})\s* # 2‑digit degrees
245+
(?P<min>\d{2})\s* # 2‑digit minutes
246+
(?P<sec>\d{2}(?:\.\d+)?)\s* # 2‑digit seconds (decimal optional)
247+
$""",
248+
re.IGNORECASE | re.VERBOSE,
249+
)
250+
251+
252+
def dms_to_dd(dms: str) -> float:
253+
"""
254+
Convert a compact DMS string (e.g. 'N382623.45') to decimal degrees.
255+
256+
Parameters
257+
----------
258+
dms : str
259+
Compact or “classic” DMS representation.
260+
261+
Returns
262+
-------
263+
float
264+
Positive for North / East, negative for South / West.
265+
266+
Raises
267+
------
268+
ValueError
269+
If the string cannot be parsed.
270+
"""
271+
match = _dms_re.match(dms)
272+
if not match:
273+
raise ValueError(f"Invalid compact DMS: {dms!r}")
274+
275+
# Pull out the numeric parts
276+
deg = float(match.group("deg"))
277+
minu = float(match.group("min"))
278+
sec = float(match.group("sec"))
279+
280+
# DMS → DD formula
281+
dd = deg + minu / 60.0 + sec / 3600.0
282+
283+
# Decide the sign
284+
direction = match.group("dir")
285+
if direction:
286+
direction = direction.upper()
287+
sign = -1 if direction in ("S", "W") else 1
288+
else:
289+
sign = 1
290+
if dms.lstrip().startswith("-"):
291+
sign = -1
292+
293+
return sign * dd
294+
295+
296+
def dd_to_mgrs(lat: float, lon: float, prec: int = 5) -> str:
297+
if sys.version_info < (3, 13):
298+
mgrs_converter = mgrs.MGRS()
299+
return mgrs_converter.toMGRS(lat, lon, MGRSPrecision=prec)
300+
else:
301+
ll_coords = LLtoUTM(lat, lon)
302+
return encode(ll_coords, 5)
303+
304+
305+
def mgrs_to_dd(value: str) -> tuple[float, float]:
306+
if sys.version_info < (3, 13):
307+
mgrs_converter = mgrs.MGRS()
308+
return mgrs_converter.toLatLon(value, inDegrees=True)
309+
else:
310+
ll_coords = UTMtoLL(decode(value))
311+
return ll_coords['lat'], ll_coords['lon']
141312

142313

143314
def get_active_runways(runways, wind):

extensions/lotatc/extension.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import luadata
99
import os
10+
import platform
1011
import re
1112
import shutil
1213
import ssl
@@ -17,7 +18,7 @@
1718
from core import Extension, utils, Server, ServiceRegistry, get_translation, InstallException
1819
from discord.ext import tasks
1920
from extensions.srs import SRS
20-
from packaging import version as ver
21+
from packaging.version import parse
2122
from services.bot import BotService
2223
from services.servicebus import ServiceBus
2324
from typing import Optional, cast
@@ -49,6 +50,12 @@ class LotAtc(Extension, FileSystemEventHandler):
4950
def __init__(self, server: Server, config: dict):
5051
self.home = os.path.join(server.instance.home, 'Mods', 'Services', 'LotAtc')
5152
super().__init__(server, config)
53+
# check version incompatibility
54+
if parse(self.version) >= parse('2.5.0') and sys.platform == 'win32':
55+
winver = platform.win32_ver()
56+
if winver[1] == '10.0.14393' and 'Server' in winver[3]:
57+
raise InstallException("LotAtc 2.5+ does not run on Windows Server 2016 anymore!")
58+
5259
self.observer: Optional[Observer] = None
5360
self.bus = ServiceRegistry.get(ServiceBus)
5461
self.gcis = {
@@ -217,7 +224,7 @@ def is_running(self) -> bool:
217224
def get_inst_version(self) -> tuple[str, str]:
218225
path = os.path.join(self.get_inst_path(), 'server')
219226
versions = os.listdir(path)
220-
major_version = max(versions, key=ver.parse)
227+
major_version = max(versions, key=parse)
221228
path = os.path.join(path, major_version, 'Mods', 'services', 'LotAtc', 'bin')
222229
version = utils.get_windows_version(os.path.join(path, 'LotAtc.dll'))
223230
return major_version, version

plugins/funkman/commands.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ def read_locals(self) -> dict:
2121
raise PluginInstallationError(self.plugin_name,
2222
f"Can't find {self.node.config_dir}/plugins/funkman.yaml, "
2323
f"please create one!")
24-
path = config.get(DEFAULT_TAG, {}).get('install')
25-
if not path or not os.path.exists(path):
26-
raise PluginInstallationError(self.plugin_name,
27-
f"FunkMan install path is not set correctly in the DEFAULT-section of "
28-
f"your {self.plugin_name}.yaml! FunkMan will not work.")
2924
return config
3025

3126
async def install(self) -> bool:

plugins/funkman/listener.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import asyncio
22
import discord
3-
import sys
43
import uuid
54
import matplotlib.figure
65

76
from core import EventListener, Server, event, Player, PersistentReport, Channel, get_translation
7+
from funkman.funkplot.funkplot import FunkPlot
8+
from funkman.utils.utils import _GetVal
89
from io import BytesIO
910
from matplotlib import pyplot as plt
1011
from typing import Literal, TYPE_CHECKING
@@ -22,17 +23,13 @@ class FunkManEventListener(EventListener["FunkMan"]):
2223
def __init__(self, plugin: "FunkMan"):
2324
super().__init__(plugin)
2425
self.config = self.get_config()
25-
path = self.config.get('install')
26-
sys.path.append(path)
27-
from funkman.utils.utils import _GetVal
2826
self.funkplot = None
2927
self._GetVal = _GetVal
3028
self.lock = asyncio.Lock()
3129

3230
async def get_funkplot(self):
3331
async with self.lock:
3432
if not self.funkplot:
35-
from funkman.funkplot.funkplot import FunkPlot
3633
self.funkplot = FunkPlot(ImagePath=self.config['IMAGEPATH'])
3734
return self.funkplot
3835

plugins/greenieboard/listener.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import psycopg
55
import re
66
import string
7-
import sys
87

98
from core import EventListener, Server, Player, Channel, Side, PersistentReport, event, get_translation, utils
9+
from funkman.funkplot.funkplot import FunkPlot
1010
from matplotlib import pyplot as plt
1111
from pathlib import Path
1212
from plugins.creditsystem.player import CreditPlayer
@@ -39,13 +39,6 @@ def __init__(self, plugin: "GreenieBoard"):
3939
super().__init__(plugin)
4040
config = self.get_config()
4141
if 'FunkMan' in config:
42-
path = config['FunkMan']['install']
43-
if not os.path.exists(path):
44-
self.log.error(f"FunkMan install path is not correct in your {self.plugin_name}.yaml! "
45-
f"FunkMan will not work.")
46-
return
47-
sys.path.append(path)
48-
from funkman.funkplot.funkplot import FunkPlot
4942
self.funkplot = FunkPlot(ImagePath=config['FunkMan']['IMAGEPATH'])
5043
else:
5144
self.funkplot = None

0 commit comments

Comments
 (0)