Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 100 additions & 14 deletions qBRA/dockwidgets/ils/ils_llz_dockwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def __init__(self, iface_):
self._widget = uic.loadUi(UI_PATH)
self.setWidget(self._widget)
self._wire()
self._init_facility()
self._init_mode_and_facilities()
self.refresh_layers()

def defaultArea(self):
Expand All @@ -35,36 +35,83 @@ def _wire(self):
self._widget.btnDirection.setProperty("direction", "forward")
self._widget.btnDirection.setText("Direction: Start to End")

def _init_facility(self):
self._facility_defs = {
def _init_mode_and_facilities(self):
# Directional facilities
self._facility_defs_dir = {
# key: (label, a_depends_threshold, defaults)
"LOC": ("ILS LLZ – single frequency", True, {"b": 500, "h": 70, "D": 500, "H": 10, "L": 2300, "phi": 30, "r_expr": "a+6000"}),
"LOCII": ("ILS LLZ – dual frequency", True, {"b": 500, "h": 70, "D": 500, "H": 20, "L": 1500, "phi": 20, "r_expr": "a+6000"}),
"GP": ("ILS GP M-Type (dual)", False, {"a": 800, "b": 50, "h": 70, "D": 250, "H": 5, "L": 325, "phi": 10, "r": 6000}),
"DME": ("DME (directional)", True, {"b": 20, "h": 70, "D": 600, "H": 20, "L": 1500, "phi": 40, "r_expr": "a+6000"}),
}
# Omnidirectional facilities presets (initial set)
self._facility_defs_omni = {
# key: (label, defaults for r, alpha, R, optional j/h)
"OMNI_DME_N": ("DME N (omnidirectional)", {"r": 300, "alpha": 1.0, "R": 3000}),
"OMNI_CVOR": ("CVOR (omnidirectional)", {"r": 600, "alpha": 1.0, "R": 3000, "j": 15000, "h": 52}),
"OMNI_DVOR": ("DVOR (omnidirectional)", {"r": 600, "alpha": 1.0, "R": 3000, "j": 10000, "h": 52}),
"OMNI_DF": ("Direction Finder (omnidirectional)", {"r": 500, "alpha": 1.0, "R": 3000, "j": 10000, "h": 52}),
"OMNI_MARKERS": ("Markers (omnidirectional)", {"r": 50, "alpha": 20.0, "R": 200}),
"OMNI_NDB": ("NDB (omnidirectional)", {"r": 200, "alpha": 5.0, "R": 1000}),
"OMNI_GBAS_REF": ("GBAS ground Reference receiver", {"r": 400, "alpha": 3.0, "R": 3000}),
"OMNI_GBAS_VDB": ("GBAS VDB station", {"r": 300, "alpha": 0.9, "R": 3000}),
"OMNI_VDB_MON": ("VDB station monitoring station", {"r": 400, "alpha": 3.0, "R": 3000}),
"OMNI_VHF_TX": ("VHF Communication Tx", {"r": 300, "alpha": 1.0, "R": 2000}),
"OMNI_VHF_RX": ("VHF Communication Rx", {"r": 300, "alpha": 1.0, "R": 2000}),
"OMNI_PSR": ("PSR (surveillance)", {"r": 500, "alpha": 0.25, "R": 15000}),
"OMNI_SSR": ("SSR (surveillance)", {"r": 500, "alpha": 0.25, "R": 15000}),
}

# connect handlers
self._widget.cboMode.currentIndexChanged.connect(self._on_mode_changed)
self._widget.cboFacility.currentIndexChanged.connect(self._on_facility_changed)
self._widget.spnA.valueChanged.connect(self._maybe_update_r)
self._widget.chkOmniTurbine.toggled.connect(self._on_turbine_toggle)
# initialize
self._on_mode_changed()

def _on_mode_changed(self):
mode_text = self._widget.cboMode.currentText() or "Directional"
is_omni = mode_text.lower().startswith("omni")
# toggle parameter groups
self._widget.grpParameters.setVisible(not is_omni)
self._widget.grpOmniParameters.setVisible(is_omni)
# populate facilities
cb = self._widget.cboFacility
cb.blockSignals(True)
cb.clear()
for key, (label, _dep, _defs) in self._facility_defs.items():
cb.addItem(label, key)
cb.currentIndexChanged.connect(self._apply_facility_defaults)
# Update r when A changes for types where r depends on a
self._widget.spnA.valueChanged.connect(self._maybe_update_r)
# Set initial
cb.setCurrentIndex(0)
self._apply_facility_defaults()
if is_omni:
for key, (label, _defs) in self._facility_defs_omni.items():
cb.addItem(label, key)
else:
for key, (label, _dep, _defs) in self._facility_defs_dir.items():
cb.addItem(label, key)
cb.blockSignals(False)
# apply defaults for the initial selection
self._on_facility_changed()

def _on_turbine_toggle(self, checked):
self._set_turbine_fields(enabled=checked, reset=False)

def _on_facility_changed(self):
mode_text = self._widget.cboMode.currentText() or "Directional"
is_omni = mode_text.lower().startswith("omni")
if is_omni:
self._apply_omni_defaults()
else:
self._apply_facility_defaults()

def _maybe_update_r(self):
key = self._widget.cboFacility.currentData()
defs = self._facility_defs.get(key, (None, False, {}))[2]
defs = self._facility_defs_dir.get(key, (None, False, {}))[2]
r_expr = defs.get("r_expr")
if r_expr == "a+6000":
a = float(self._widget.spnA.value())
self._widget.spnr.setValue(a + 6000.0)

def _apply_facility_defaults(self):
key = self._widget.cboFacility.currentData()
label, a_dep, defs = self._facility_defs.get(key, ("", False, {}))
label, a_dep, defs = self._facility_defs_dir.get(key, ("", False, {}))
# A: if explicitly present in defaults, set; if depends on threshold, try to estimate from routing start
if "a" in defs:
self._widget.spnA.setValue(float(defs["a"]))
Expand Down Expand Up @@ -100,6 +147,31 @@ def _apply_facility_defaults(self):
else:
self._maybe_update_r()

def _apply_omni_defaults(self):
key = self._widget.cboFacility.currentData()
defs = self._facility_defs_omni.get(key, ("", {}))[1]
self._widget.spnOmni_r.setValue(float(defs.get("r", 0)))
self._widget.spnOmni_alpha.setValue(float(defs.get("alpha", 1)))
self._widget.spnOmni_R.setValue(float(defs.get("R", 0)))
has_turbine = ("j" in defs and "h" in defs)
# block signal to avoid resetting user toggles when applying defaults
self._widget.chkOmniTurbine.blockSignals(True)
self._widget.chkOmniTurbine.setChecked(has_turbine)
self._widget.chkOmniTurbine.blockSignals(False)
self._set_turbine_fields(enabled=has_turbine, preset_j=defs.get("j"), preset_h=defs.get("h"), reset=True)

def _set_turbine_fields(self, enabled, preset_j=None, preset_h=None, reset=False):
self._widget.spnOmni_j.setEnabled(enabled)
self._widget.spnOmni_h.setEnabled(enabled)
if not enabled:
# keep values but make them inert when toggle is off
return
if reset:
if preset_j is not None:
self._widget.spnOmni_j.setValue(float(preset_j))
if preset_h is not None:
self._widget.spnOmni_h.setValue(float(preset_h))

def _toggle_direction(self):
current = self._widget.btnDirection.property("direction") or "forward"
new = "backward" if current == "forward" else "forward"
Expand Down Expand Up @@ -242,8 +314,10 @@ def _format_runway(val):
custom_name = (self._widget.txtOutputName.text() or "").strip()
base_name = custom_name if custom_name else remark
display_name = f"{base_name} - {facility_label}" if facility_label else base_name
mode_text = self._widget.cboMode.currentText() or "Directional"
is_omni = mode_text.lower().startswith("omni")

return {
params = {
"active_layer": navaid_layer,
"azimuth": azimuth,
"a": a,
Expand All @@ -261,3 +335,15 @@ def _format_runway(val):
"facility_label": facility_label,
"display_name": display_name,
}

if is_omni:
params.update({
"omni_r": float(self._widget.spnOmni_r.value()),
"omni_alpha": float(self._widget.spnOmni_alpha.value()),
"omni_R": float(self._widget.spnOmni_R.value()),
"omni_turbine": bool(self._widget.chkOmniTurbine.isChecked()),
"omni_j": float(self._widget.spnOmni_j.value()),
"omni_h": float(self._widget.spnOmni_h.value()),
})

return params
135 changes: 135 additions & 0 deletions qBRA/modules/ils_llz_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
QgsPolygon,
QgsLineString,
)
from math import tan, radians, cos, sin, pi
from qgis.PyQt.QtCore import QVariant
from qgis.PyQt.QtGui import QColor

Expand Down Expand Up @@ -270,3 +271,137 @@ def pz(point, z):
z_layer.updateExtents()

return z_layer


def build_layers_omni(iface, params):
"""
Build omnidirectional BRA shapes as 2D footprints:
- Inner cylinder: circle of radius r
- Outer cone footprint: circle of radius R
- Optional turbine analysis cylinder: circle of radius j
Attributes include r, alpha, R, j, h and type (last column).
"""
layer = params["active_layer"]
selection = layer.selectedFeatures()
if not selection:
raise ValueError("Select one feature on the active layer")
feat = selection[0]
p_geom = feat.geometry().asPoint()

map_srid = iface.mapCanvas().mapSettings().destinationCrs().authid()

display_name = params.get("display_name") or params.get("remark") or "BRA"
r = float(params.get("omni_r", 0.0))
alpha = float(params.get("omni_alpha", 1.0))
R = float(params.get("omni_R", 0.0))
turbine = bool(params.get("omni_turbine", False))
j = float(params.get("omni_j", 0.0)) if turbine else 0.0
h = float(params.get("omni_h", 0.0)) if turbine else 0.0
base_z = float(params.get("site_elev", 0.0))

if r <= 0 or R <= 0:
raise ValueError("Omni parameters invalid: r and R must be > 0")
if R < r:
raise ValueError("Omni parameter invalid: R must be >= r")
if alpha <= 0 or alpha > 90:
raise ValueError("Omni parameter invalid: alpha must be in (0, 90]")
if turbine:
if j <= 0 or h <= 0:
raise ValueError("Omni turbine parameters invalid: j and h must be > 0")
if j < r:
raise ValueError("Omni turbine parameter invalid: j must be >= r")

# Heights from cone geometry (Figure 2.1/2.2): z = radius * tan(alpha)
alpha_rad = radians(alpha)
h_cone_outer = R * tan(alpha_rad)
h_cone_inner = r * tan(alpha_rad)

segments = 128

# Create memory layer for 3D polygons
layer_out = QgsVectorLayer("PolygonZ?crs=" + map_srid, f"{display_name} BRA_omni", "memory")
fields = [
QgsField("id", QVariant.Int),
QgsField("area", QVariant.String),
QgsField("area_name", QVariant.String),
QgsField("r", QVariant.String),
QgsField("alpha", QVariant.String),
QgsField("R", QVariant.String),
QgsField("j", QVariant.String),
QgsField("h", QVariant.String),
QgsField("type", QVariant.String),
]
pr = layer_out.dataProvider()
pr.addAttributes(fields)
layer_out.updateFields()

_type_value = params.get("facility_label") or params.get("facility_key") or ""

def circle_points(center_pt, radius, z_value):
pts = []
cx = center_pt.x()
cy = center_pt.y()
for i in range(segments):
ang = 2.0 * pi * i / segments
x = cx + radius * cos(ang)
y = cy + radius * sin(ang)
pts.append(QgsPoint(x, y, z_value + base_z))
pts.append(pts[0])
return pts

# Inner cylinder top (flat disk at z = h_cone_inner)
inner_ring = circle_points(p_geom, r, h_cone_inner)
f1 = QgsFeature()
f1.setGeometry(QgsGeometry(QgsPolygon(QgsLineString(inner_ring), rings=[])))
f1.setAttributes([
1,
"inner cylinder top",
display_name,
str(r),
str(alpha),
str(R),
str(j if turbine else 0.0),
str(h if turbine else 0.0),
_type_value,
])
pr.addFeatures([f1])

# Cone mantle approximated as polygon with outer ring at z=h_cone_outer and inner ring at z=h_cone_inner
outer_ring = circle_points(p_geom, R, h_cone_outer)
inner_ring_cone = circle_points(p_geom, r, h_cone_inner)[::-1] # reverse for hole
f2 = QgsFeature()
f2.setGeometry(QgsGeometry(QgsPolygon(QgsLineString(outer_ring), rings=[QgsLineString(inner_ring_cone)])))
f2.setAttributes([
2,
"cone mantle",
display_name,
str(r),
str(alpha),
str(R),
str(j if turbine else 0.0),
str(h if turbine else 0.0),
_type_value,
])
pr.addFeatures([f2])

# Optional turbine cylinder top at height h
if turbine and j > 0:
turbine_ring = circle_points(p_geom, j, h)
f3 = QgsFeature()
f3.setGeometry(QgsGeometry(QgsPolygon(QgsLineString(turbine_ring), rings=[])))
f3.setAttributes([
3,
"turbine cylinder top",
display_name,
str(r),
str(alpha),
str(R),
str(j),
str(h),
_type_value,
])
pr.addFeatures([f3])

layer_out.triggerRepaint()
layer_out.updateExtents()
return layer_out
8 changes: 6 additions & 2 deletions qBRA/qbra_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from qgis.utils import iface

from .dockwidgets.ils.ils_llz_dockwidget import IlsLlzDockWidget
from .modules.ils_llz_logic import build_layers
from .modules.ils_llz_logic import build_layers, build_layers_omni
import os

class QbraPlugin(QObject):
Expand Down Expand Up @@ -66,7 +66,11 @@ def _on_calculate(self):
self.iface.messageBar().pushMessage("QBRA", "Invalid inputs", level=Qgis.Warning)
return
try:
result_layer = build_layers(self.iface, params)
mode = params.get("mode", "directional")
if mode == "omni":
result_layer = build_layers_omni(self.iface, params)
else:
result_layer = build_layers(self.iface, params)
except Exception as exc:
self.iface.messageBar().pushMessage("QBRA", f"Error: {exc}", level=Qgis.Critical)
return
Expand Down
Loading