refactoring di preview e simulazione usando traiettorie calcolate
This commit is contained in:
parent
288c4f73be
commit
1575c1b38a
@ -1,101 +1,100 @@
|
|||||||
x_nm,y_nm
|
x_nm,y_nm
|
||||||
0.17452406437283513,9.998476951563912
|
1.7364817766693033,9.84807753012208
|
||||||
0.2859422147817867,10.279638104623308
|
1.8196915585801245,9.997286852043835
|
||||||
0.4421393353133215,10.609933583832394
|
1.909698355298528,10.164705993254746
|
||||||
0.636432547474389,10.985187585712282
|
2.006314644389864,10.349354914685591
|
||||||
0.8621389727719386,11.401224306784082
|
2.1093529034194827,10.550253577267155
|
||||||
1.1125757327129193,11.853867943568899
|
2.218625609952734,10.766421941930222
|
||||||
1.3810599488042814,12.338942692587844
|
2.3339452415549684,10.99687996960557
|
||||||
1.6609087425529734,12.852272750362022
|
2.4551242757915364,11.240647621223985
|
||||||
1.9454392354659444,13.389682313412546
|
2.581975190227787,11.496744857716246
|
||||||
2.227968549050145,13.946995578260525
|
2.714310462429072,11.764191640013136
|
||||||
2.5018138048125236,14.520036741427065
|
2.851942569960741,12.04200792904544
|
||||||
2.7602921242600305,15.104629999433275
|
2.9946839903881437,12.329213685743936
|
||||||
2.996720628899614,15.696599548800267
|
3.14234720127663,12.624828871039403
|
||||||
3.2044164402382243,16.291769586049142
|
3.2947446801915516,12.927873445862636
|
||||||
3.3766966797828104,16.885964307701016
|
3.4516889046982575,13.237367371144403
|
||||||
3.5068784690403216,17.475007910277
|
3.612992352362098,13.552330607815493
|
||||||
3.588278929517707,18.054724590298196
|
3.7784675007484245,13.871783116806688
|
||||||
3.6142151827219164,18.620938544285714
|
3.947926827422586,14.19474485904877
|
||||||
3.578004350159902,19.169473968760666
|
4.121182809949932,14.520235795472518
|
||||||
3.472963553338606,19.696155060244156
|
4.298047925895815,14.847275887008719
|
||||||
3.4729635533386065,19.69615506024416
|
4.478334652825583,15.17488509458815
|
||||||
3.272762644392875,20.217383756990923
|
4.661855468304586,15.502083379141595
|
||||||
2.964843068011936,20.750866222507394
|
4.848422849898178,15.827890701599843
|
||||||
2.5631793345343286,21.294382991192247
|
5.037849275171705,16.151327022893664
|
||||||
2.081745954298589,21.845714597444157
|
5.2299472216905185,16.471412303953848
|
||||||
1.5345174376432555,22.402641575661793
|
5.424529167019969,16.787166505711173
|
||||||
0.9354682949068648,22.962944460243826
|
5.621407588725407,17.09760958909643
|
||||||
0.2985730364279542,23.52440378558893
|
5.8203949643721815,17.401761515040384
|
||||||
-0.36219382745493856,24.084800086095772
|
6.021303771525644,17.698642244473834
|
||||||
-1.032857786403277,24.641913896163032
|
6.223946487751144,17.987271738327554
|
||||||
-1.699444330078522,25.193525750189377
|
6.4281355906140325,18.26666995753233
|
||||||
-2.347978948142139,25.737416182573483
|
6.6336835576796584,18.535856863018942
|
||||||
-2.964487130255587,26.271365727714013
|
6.840402866513372,18.79385241571817
|
||||||
-3.5349943660803307,26.793154920009652
|
6.840402866513374,18.79385241571817
|
||||||
-4.045526145277833,27.30056429385906
|
7.064412663195302,19.043619595904882
|
||||||
-4.482107957509558,27.79137438366092
|
7.319986102939524,19.28896147814137
|
||||||
-4.830765292436963,28.263365723813894
|
7.604166677012322,19.530158164789555
|
||||||
-5.077523639721516,28.714318848716662
|
7.913997876679983,19.767489758211376
|
||||||
-5.208408489024677,29.14201429276789
|
8.246523193208787,20.001236360768743
|
||||||
-5.2094453300079095,29.544232590366253
|
8.598786117865023,20.231678074823588
|
||||||
-5.2094453300079095,29.544232590366242
|
8.96783014191497,20.459095002737843
|
||||||
-5.087215806911271,29.93160758132983
|
9.350698756624908,20.683767246873423
|
||||||
-4.8634013589893685,30.31504050771131
|
9.744435453261131,20.90597490959226
|
||||||
-4.54667716842549,30.69271300726126
|
10.146083723089918,21.12599809325628
|
||||||
-4.145718417402923,31.062806717730222
|
10.552687057377549,21.3441169002274
|
||||||
-3.6692002881049564,31.42350327686877
|
10.961288947390312,21.560611432867567
|
||||||
-3.1257979627148766,31.772984322427458
|
11.368932884394491,21.775761793538678
|
||||||
-2.5241866234159707,32.10943149215684
|
11.772662359656369,21.989848084602684
|
||||||
-1.873041452391528,32.43102642380748
|
12.169520864442228,22.203150408421493
|
||||||
-1.1810376318248348,32.735950755129934
|
12.556551890018351,22.41594886735704
|
||||||
-0.4568503438991798,33.02238612387475
|
12.930798927651024,22.62852356377125
|
||||||
0.29084522920215017,33.28851416779252
|
13.289305468606532,22.84115460002604
|
||||||
1.0533739052958677,33.53251652463377
|
13.629115004151155,23.054122078483353
|
||||||
1.822060502198683,33.75257483214907
|
13.947271025551181,23.267706101505095
|
||||||
2.5882298377273103,33.94687072808898
|
14.24081702407289,23.482186771453204
|
||||||
3.343206729698462,34.113585850204075
|
14.506796490982568,23.6978441906896
|
||||||
4.078315995928851,34.25090183624488
|
14.742252917546494,23.91495846157622
|
||||||
4.784882454235189,34.35700032396197
|
14.944229795030958,24.133809686474972
|
||||||
5.454230922434187,34.430062951105924
|
15.109770614702246,24.354677967747794
|
||||||
6.077686218342558,34.46827135542727
|
15.23591886782663,24.57784340775661
|
||||||
6.077686218342562,34.468271355427284
|
15.319718045670406,24.80358610886334
|
||||||
6.6950615437258465,34.47971290071672
|
15.358211639499848,25.032186173429917
|
||||||
7.3476044195818755,34.472832854810775
|
15.348443140581244,25.26392370381826
|
||||||
8.028791142575807,34.44634971152879
|
15.28745604018088,25.499078802390304
|
||||||
8.732098009372796,34.3989819646901
|
15.172293829565035,25.737931571507964
|
||||||
9.451001316637997,34.32944810811404
|
15.0,25.980762113533167
|
||||||
10.178977361036566,34.23646663561996
|
14.999999999999998,25.98076211353316
|
||||||
10.909502439233654,34.11875604102719
|
14.76007873005941,26.230939963143115
|
||||||
11.636052847894423,33.97503481815507
|
14.447076186168827,26.491050501357915
|
||||||
12.352104883684024,33.80402146082294
|
14.065598841372388,26.760197600053854
|
||||||
13.051134843267612,33.60443446285013
|
13.62025316871424,27.037485131107207
|
||||||
13.726619023310347,33.37499231805599
|
13.11564564123853,27.322016966394273
|
||||||
14.372033720477376,33.114413520259845
|
12.556382731989396,27.612896977791323
|
||||||
14.98085523143386,32.82141656328105
|
11.947070914010986,27.909229037174644
|
||||||
15.546559852844958,32.49471994093893
|
11.292316660347446,28.210117016420522
|
||||||
16.062623881375814,32.13304214705283
|
10.596726444042913,28.514664787405238
|
||||||
16.522523613691593,31.735101675442085
|
9.86490673814154,28.82197622200508
|
||||||
16.919735346457447,31.29961701992603
|
9.101464015687464,29.13115519209633
|
||||||
17.247735376338525,30.825306674324008
|
8.311004749724834,29.441305569555276
|
||||||
17.499999999999996,30.310889132455358
|
7.498135413297791,29.751531226258194
|
||||||
17.499999999999996,30.310889132455355
|
6.66746247945048,30.060936034081376
|
||||||
17.667951456825268,29.722378176057024
|
5.823592421227048,30.368623864901103
|
||||||
17.752649951768674,29.034858901953363
|
4.971131711671633,30.67369859059366
|
||||||
17.76202872985173,28.26065677659477
|
4.114686823828384,30.97526408303533
|
||||||
17.704021036095945,27.412097266431687
|
3.2588642307414455,31.272424214102394
|
||||||
17.586560115522854,26.501505837914515
|
2.4082704054549575,31.564282855671145
|
||||||
17.417579213153953,25.541207957493693
|
1.5675118210130687,31.84994387961786
|
||||||
17.205011574010783,24.54352909161962
|
0.7411949504599216,32.12851115781883
|
||||||
16.956790443114837,23.52079470674273
|
-0.06607373316034426,32.39908856215034
|
||||||
16.68084906548765,22.485330269313444
|
-0.8496877568035757,32.660779964488654
|
||||||
16.385120686150735,21.449461245782175
|
-1.6050406474256338,32.91268923671007
|
||||||
16.0775385501256,20.425513102599343
|
-2.3275259319823753,33.15392025069089
|
||||||
15.766035902433776,19.42581130621538
|
-3.0125371374296517,33.38357687830738
|
||||||
15.45854598809677,18.462681323080695
|
-3.6554677907233284,33.600762991435815
|
||||||
15.1630020521361,17.5484486196457
|
-4.2517114188192515,33.80458246195249
|
||||||
14.88733733957329,16.69543866236083
|
-4.796661548673278,33.99413916173369
|
||||||
14.639485095429851,15.9159769176765
|
-5.285711707241269,34.1685369626557
|
||||||
14.427378564727302,15.222388852043133
|
-5.714255421479081,34.326879736594805
|
||||||
14.258950992487165,14.626999931911136
|
-6.077686218342556,34.46827135542728
|
||||||
14.142135623730947,14.142135623730947
|
|
||||||
|
|||||||
|
@ -3,7 +3,7 @@
|
|||||||
"scan_limit": 60,
|
"scan_limit": 60,
|
||||||
"max_range": 100,
|
"max_range": 100,
|
||||||
"geometry": "1200x1024+463+195",
|
"geometry": "1200x1024+463+195",
|
||||||
"last_selected_scenario": "scenario3",
|
"last_selected_scenario": "scenario1",
|
||||||
"connection": {
|
"connection": {
|
||||||
"target": {
|
"target": {
|
||||||
"type": "tftp",
|
"type": "tftp",
|
||||||
@ -47,10 +47,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"maneuver_type": "Fly for Duration",
|
"maneuver_type": "Fly for Duration",
|
||||||
|
"duration_s": 100.0,
|
||||||
|
"target_altitude_ft": 10000.0,
|
||||||
"target_velocity_fps": 1670.9318999999994,
|
"target_velocity_fps": 1670.9318999999994,
|
||||||
"target_heading_deg": 10.0,
|
"target_heading_deg": 10.0
|
||||||
"duration_s": 300.0,
|
|
||||||
"target_altitude_ft": 10000.0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"maneuver_type": "Fly to Point",
|
"maneuver_type": "Fly to Point",
|
||||||
@ -59,7 +59,8 @@
|
|||||||
"target_range_nm": 25.0,
|
"target_range_nm": 25.0,
|
||||||
"target_azimuth_deg": -20.0
|
"target_azimuth_deg": -20.0
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"use_spline": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -150,7 +151,7 @@
|
|||||||
"target_azimuth_deg": -10.0
|
"target_azimuth_deg": -10.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"use_spline": true
|
"use_spline": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -164,29 +165,43 @@
|
|||||||
"trajectory": [
|
"trajectory": [
|
||||||
{
|
{
|
||||||
"maneuver_type": "Fly to Point",
|
"maneuver_type": "Fly to Point",
|
||||||
"target_velocity_fps": 506.343,
|
|
||||||
"target_heading_deg": 180.0,
|
|
||||||
"duration_s": 10.0,
|
"duration_s": 10.0,
|
||||||
"target_altitude_ft": 10000.0,
|
"target_altitude_ft": 10000.0,
|
||||||
"target_range_nm": 20.0,
|
"target_velocity_fps": 506.343,
|
||||||
|
"target_heading_deg": 180.0,
|
||||||
|
"target_range_nm": 5.0,
|
||||||
"target_azimuth_deg": 0.0
|
"target_azimuth_deg": 0.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"maneuver_type": "Fly for Duration",
|
"maneuver_type": "Fly for Duration",
|
||||||
"target_velocity_fps": 506.343,
|
|
||||||
"target_heading_deg": 90.0,
|
|
||||||
"duration_s": 20.0,
|
"duration_s": 20.0,
|
||||||
"target_altitude_ft": 10000.0
|
"target_altitude_ft": 10000.0,
|
||||||
|
"target_velocity_fps": 506.343,
|
||||||
|
"target_heading_deg": 90.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"maneuver_type": "Fly for Duration",
|
"maneuver_type": "Fly for Duration",
|
||||||
"target_velocity_fps": 506.343,
|
|
||||||
"target_heading_deg": 180.0,
|
|
||||||
"duration_s": 10.0,
|
"duration_s": 10.0,
|
||||||
"target_altitude_ft": 10000.0
|
"target_altitude_ft": 10000.0,
|
||||||
|
"target_velocity_fps": 506.343,
|
||||||
|
"target_heading_deg": 180.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maneuver_type": "Fly for Duration",
|
||||||
|
"duration_s": 20.0,
|
||||||
|
"target_altitude_ft": 10000.0,
|
||||||
|
"target_velocity_fps": 506.343,
|
||||||
|
"target_heading_deg": 90.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maneuver_type": "Fly for Duration",
|
||||||
|
"duration_s": 30.0,
|
||||||
|
"target_altitude_ft": 10000.0,
|
||||||
|
"target_velocity_fps": 506.343,
|
||||||
|
"target_heading_deg": -180.0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"use_spline": false
|
"use_spline": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,14 +14,15 @@ logger = get_logger(__name__)
|
|||||||
def build_tgtinit(target: Target) -> str:
|
def build_tgtinit(target: Target) -> str:
|
||||||
"""
|
"""
|
||||||
Builds the 'tgtinit' command from a target's initial state.
|
Builds the 'tgtinit' command from a target's initial state.
|
||||||
|
After the refactor, the target object's state is pre-calculated
|
||||||
|
by reset_simulation(), so we can read kinematics directly.
|
||||||
"""
|
"""
|
||||||
params = [
|
params = [
|
||||||
target.target_id,
|
target.target_id,
|
||||||
f"{target.current_range_nm:.2f}",
|
f"{target.current_range_nm:.2f}",
|
||||||
f"{target.current_azimuth_deg:.2f}",
|
f"{target.current_azimuth_deg:.2f}",
|
||||||
# Velocity and heading for tgtinit are taken from the first waypoint
|
f"{target.current_velocity_fps:.2f}",
|
||||||
f"{(target.trajectory[0].target_velocity_fps or 0.0) if target.trajectory else 0.0:.2f}",
|
f"{target.current_heading_deg:.2f}",
|
||||||
f"{(target.trajectory[0].target_heading_deg or 0.0) if target.trajectory else 0.0:.2f}",
|
|
||||||
f"{target.current_altitude_ft:.2f}"
|
f"{target.current_altitude_ft:.2f}"
|
||||||
]
|
]
|
||||||
qualifiers = ["/s" if target.active else "/-s", "/t" if target.traceable else "/-t"]
|
qualifiers = ["/s" if target.active else "/-s", "/t" if target.traceable else "/-t"]
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from dataclasses import dataclass, field, fields
|
from dataclasses import dataclass, field, fields
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
# --- Constants ---
|
# --- Constants ---
|
||||||
MIN_TARGET_ID = 0
|
MIN_TARGET_ID = 0
|
||||||
@ -25,28 +25,23 @@ class Waypoint:
|
|||||||
"""Represents a segment of a target's trajectory, defining a maneuver."""
|
"""Represents a segment of a target's trajectory, defining a maneuver."""
|
||||||
maneuver_type: ManeuverType = ManeuverType.FLY_FOR_DURATION
|
maneuver_type: ManeuverType = ManeuverType.FLY_FOR_DURATION
|
||||||
|
|
||||||
# Parameters used by FLY_FOR_DURATION and as final state for FLY_TO_POINT
|
# Parameters used by both maneuver types
|
||||||
target_velocity_fps: Optional[float] = None
|
|
||||||
target_heading_deg: Optional[float] = None
|
|
||||||
|
|
||||||
# Parameters used by both
|
|
||||||
duration_s: Optional[float] = 10.0
|
duration_s: Optional[float] = 10.0
|
||||||
target_altitude_ft: Optional[float] = 10000.0
|
target_altitude_ft: Optional[float] = 10000.0
|
||||||
|
|
||||||
# Parameters used by FLY_TO_POINT
|
# Final state parameters for FLY_TO_POINT and dynamic for FLY_FOR_DURATION
|
||||||
|
target_velocity_fps: Optional[float] = None
|
||||||
|
target_heading_deg: Optional[float] = None
|
||||||
|
|
||||||
|
# Position parameters for FLY_TO_POINT
|
||||||
target_range_nm: Optional[float] = None
|
target_range_nm: Optional[float] = None
|
||||||
target_azimuth_deg: Optional[float] = None
|
target_azimuth_deg: Optional[float] = None
|
||||||
|
|
||||||
# Internal state for interpolation, set when waypoint becomes active
|
|
||||||
_start_velocity_fps: float = field(init=False, repr=False, default=0.0)
|
|
||||||
_start_heading_deg: float = field(init=False, repr=False, default=0.0)
|
|
||||||
_start_altitude_ft: float = field(init=False, repr=False, default=0.0)
|
|
||||||
_calculated_duration_s: Optional[float] = field(init=False, default=None, repr=False)
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
def to_dict(self) -> Dict:
|
||||||
|
"""Serializes the waypoint to a dictionary."""
|
||||||
data = {"maneuver_type": self.maneuver_type.value}
|
data = {"maneuver_type": self.maneuver_type.value}
|
||||||
# Use dataclasses.fields to iterate reliably
|
|
||||||
for f in fields(self):
|
for f in fields(self):
|
||||||
|
# Exclude private fields and the already-handled maneuver_type
|
||||||
if not f.name.startswith('_') and f.name != "maneuver_type":
|
if not f.name.startswith('_') and f.name != "maneuver_type":
|
||||||
val = getattr(self, f.name)
|
val = getattr(self, f.name)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
@ -55,13 +50,12 @@ class Waypoint:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Target:
|
class Target:
|
||||||
_spline_path: Optional[List[List[float]]] = field(init=False, default=None, repr=False)
|
|
||||||
"""Represents a dynamic 3D target with a programmable trajectory."""
|
"""Represents a dynamic 3D target with a programmable trajectory."""
|
||||||
target_id: int
|
target_id: int
|
||||||
trajectory: List[Waypoint] = field(default_factory=list)
|
trajectory: List[Waypoint] = field(default_factory=list)
|
||||||
active: bool = True
|
active: bool = True
|
||||||
traceable: bool = True
|
traceable: bool = True
|
||||||
use_spline: bool = False # Se True, la traiettoria viene interpretata come spline
|
use_spline: bool = False
|
||||||
|
|
||||||
# --- Current simulation state (dynamic) ---
|
# --- Current simulation state (dynamic) ---
|
||||||
current_range_nm: float = field(init=False, default=0.0)
|
current_range_nm: float = field(init=False, default=0.0)
|
||||||
@ -71,247 +65,211 @@ class Target:
|
|||||||
current_heading_deg: float = field(init=False, default=0.0)
|
current_heading_deg: float = field(init=False, default=0.0)
|
||||||
current_pitch_deg: float = field(init=False, default=0.0)
|
current_pitch_deg: float = field(init=False, default=0.0)
|
||||||
|
|
||||||
|
# Internal Cartesian position in feet
|
||||||
_pos_x_ft: float = field(init=False, repr=False, default=0.0)
|
_pos_x_ft: float = field(init=False, repr=False, default=0.0)
|
||||||
_pos_y_ft: float = field(init=False, repr=False, default=0.0)
|
_pos_y_ft: float = field(init=False, repr=False, default=0.0)
|
||||||
_pos_z_ft: float = field(init=False, repr=False, default=0.0)
|
_pos_z_ft: float = field(init=False, repr=False, default=0.0)
|
||||||
|
|
||||||
|
# Simulation time and pre-calculated path
|
||||||
_sim_time_s: float = field(init=False, default=0.0)
|
_sim_time_s: float = field(init=False, default=0.0)
|
||||||
_current_waypoint_index: int = field(init=False, default=-1)
|
_total_duration_s: float = field(init=False, default=0.0)
|
||||||
_time_in_waypoint_s: float = field(init=False, default=0.0)
|
# Path format: List of (timestamp_s, x_ft, y_ft, z_ft)
|
||||||
|
_path: List[Tuple[float, float, float, float]] = field(init=False, repr=False, default_factory=list)
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
|
"""Validates ID and initializes the simulation state."""
|
||||||
if not (MIN_TARGET_ID <= self.target_id <= MAX_TARGET_ID):
|
if not (MIN_TARGET_ID <= self.target_id <= MAX_TARGET_ID):
|
||||||
raise ValueError(f"Target ID must be between {MIN_TARGET_ID} and {MAX_TARGET_ID}.")
|
raise ValueError(f"Target ID must be between {MIN_TARGET_ID} and {MAX_TARGET_ID}.")
|
||||||
self.reset_simulation()
|
self.reset_simulation()
|
||||||
|
|
||||||
def reset_simulation(self):
|
def reset_simulation(self):
|
||||||
|
"""
|
||||||
|
Resets the simulation time and pre-calculates the entire trajectory path
|
||||||
|
based on the waypoints. This is the core of the refactored logic.
|
||||||
|
"""
|
||||||
self._sim_time_s = 0.0
|
self._sim_time_s = 0.0
|
||||||
self._time_in_waypoint_s = 0.0
|
self._generate_path()
|
||||||
self._current_waypoint_index = -1
|
|
||||||
|
|
||||||
if self.use_spline and self.trajectory:
|
if self._path:
|
||||||
|
# Set initial state from the first point of the generated path
|
||||||
|
initial_pos = self._path[0]
|
||||||
|
self._pos_x_ft = initial_pos[1]
|
||||||
|
self._pos_y_ft = initial_pos[2]
|
||||||
|
self._pos_z_ft = initial_pos[3]
|
||||||
|
|
||||||
|
# Calculate initial heading and velocity from the first segment
|
||||||
|
if len(self._path) > 1:
|
||||||
|
next_pos = self._path[1]
|
||||||
|
dx = next_pos[1] - self._pos_x_ft
|
||||||
|
dy = next_pos[2] - self._pos_y_ft
|
||||||
|
dz = next_pos[3] - self._pos_z_ft
|
||||||
|
dt = next_pos[0] - initial_pos[0]
|
||||||
|
|
||||||
|
dist_3d = math.sqrt(dx**2 + dy**2 + dz**2)
|
||||||
|
dist_2d = math.sqrt(dx**2 + dy**2)
|
||||||
|
|
||||||
|
self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360 if dist_2d > 0.1 else 0.0
|
||||||
|
self.current_pitch_deg = math.degrees(math.atan2(dz, dist_2d)) if dist_2d > 0.1 else 0.0
|
||||||
|
self.current_velocity_fps = dist_3d / dt if dt > 0 else 0.0
|
||||||
|
else:
|
||||||
|
self.current_heading_deg = 0.0
|
||||||
|
self.current_pitch_deg = 0.0
|
||||||
|
self.current_velocity_fps = 0.0
|
||||||
|
else:
|
||||||
|
# No trajectory, reset to origin
|
||||||
|
self._pos_x_ft = self._pos_y_ft = self._pos_z_ft = 0.0
|
||||||
|
self.current_velocity_fps = 0.0
|
||||||
|
self.current_heading_deg = 0.0
|
||||||
|
self.current_pitch_deg = 0.0
|
||||||
|
|
||||||
|
self._update_current_polar_coords()
|
||||||
|
self.active = bool(self.trajectory) # A target is active if it has a trajectory
|
||||||
|
|
||||||
|
def _generate_path(self):
|
||||||
|
"""Instance method that uses the static path generator."""
|
||||||
|
self._path, self._total_duration_s = Target.generate_path_from_waypoints(self.trajectory, self.use_spline)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_path_from_waypoints(waypoints: List[Waypoint], use_spline: bool) -> Tuple[List[Tuple[float, ...]], float]:
|
||||||
|
"""
|
||||||
|
Generates a time-stamped, Cartesian path from a list of waypoints.
|
||||||
|
This is a static method so it can be used for previews without a Target instance.
|
||||||
|
"""
|
||||||
|
path = []
|
||||||
|
total_duration_s = 0.0
|
||||||
|
if not waypoints:
|
||||||
|
return path, total_duration_s
|
||||||
|
|
||||||
|
first_wp = waypoints[0]
|
||||||
|
if first_wp.maneuver_type != ManeuverType.FLY_TO_POINT:
|
||||||
|
return path, total_duration_s
|
||||||
|
|
||||||
|
keyframes = []
|
||||||
|
current_time = 0.0
|
||||||
|
|
||||||
|
range_ft = (first_wp.target_range_nm or 0.0) * NM_TO_FT
|
||||||
|
az_rad = math.radians(first_wp.target_azimuth_deg or 0.0)
|
||||||
|
current_x = range_ft * math.sin(az_rad)
|
||||||
|
current_y = range_ft * math.cos(az_rad)
|
||||||
|
current_z = first_wp.target_altitude_ft or 0.0
|
||||||
|
|
||||||
|
current_heading = first_wp.target_heading_deg or 0.0
|
||||||
|
current_velocity = first_wp.target_velocity_fps or 0.0
|
||||||
|
|
||||||
|
keyframes.append((current_time, current_x, current_y, current_z))
|
||||||
|
|
||||||
|
for i, wp in enumerate(waypoints):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
duration = wp.duration_s or 0.0
|
||||||
|
|
||||||
|
if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
|
||||||
|
target_range_ft = (wp.target_range_nm or 0.0) * NM_TO_FT
|
||||||
|
target_az_rad = math.radians(wp.target_azimuth_deg or 0.0)
|
||||||
|
current_x = target_range_ft * math.sin(target_az_rad)
|
||||||
|
current_y = target_range_ft * math.cos(target_az_rad)
|
||||||
|
current_z = wp.target_altitude_ft or current_z
|
||||||
|
|
||||||
|
elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
|
||||||
|
velocity_to_use = wp.target_velocity_fps if wp.target_velocity_fps is not None else current_velocity
|
||||||
|
heading_to_use = wp.target_heading_deg if wp.target_heading_deg is not None else current_heading
|
||||||
|
|
||||||
|
heading_rad = math.radians(heading_to_use)
|
||||||
|
dist_moved = velocity_to_use * duration
|
||||||
|
|
||||||
|
current_x += dist_moved * math.sin(heading_rad)
|
||||||
|
current_y += dist_moved * math.cos(heading_rad)
|
||||||
|
current_z = wp.target_altitude_ft or current_z
|
||||||
|
|
||||||
|
current_heading = heading_to_use
|
||||||
|
current_velocity = velocity_to_use
|
||||||
|
|
||||||
|
total_duration_s += duration
|
||||||
|
keyframes.append((total_duration_s, current_x, current_y, current_z))
|
||||||
|
|
||||||
|
if len(keyframes) < 2:
|
||||||
|
return keyframes, total_duration_s
|
||||||
|
|
||||||
|
if use_spline and len(keyframes) >= 4:
|
||||||
from target_simulator.utils.spline import catmull_rom_spline
|
from target_simulator.utils.spline import catmull_rom_spline
|
||||||
|
|
||||||
# Convert all waypoints to cartesian points in feet
|
points_to_spline = [kf[1:] for kf in keyframes]
|
||||||
pts = []
|
num_spline_points = int(total_duration_s * 10)
|
||||||
for wp in self.trajectory:
|
|
||||||
if wp.maneuver_type == ManeuverType.FLY_TO_POINT and wp.target_range_nm is not None and wp.target_azimuth_deg is not None:
|
|
||||||
range_ft = (wp.target_range_nm or 0.0) * NM_TO_FT
|
|
||||||
az_rad = math.radians(wp.target_azimuth_deg or 0.0)
|
|
||||||
x = range_ft * math.sin(az_rad)
|
|
||||||
y = range_ft * math.cos(az_rad)
|
|
||||||
z = wp.target_altitude_ft or 0.0
|
|
||||||
pts.append([x, y, z])
|
|
||||||
|
|
||||||
if not pts:
|
splined_points = catmull_rom_spline(points_to_spline, num_points=max(num_spline_points, 100))
|
||||||
self._pos_x_ft = self._pos_y_ft = self._pos_z_ft = 0.0
|
|
||||||
self.current_velocity_fps = 0.0
|
|
||||||
self.current_heading_deg = 0.0
|
|
||||||
self._spline_path = None
|
|
||||||
return
|
|
||||||
|
|
||||||
# Generate spline path from points in feet
|
|
||||||
self._spline_path = catmull_rom_spline(pts, num_points=200)
|
|
||||||
self._spline_index = 0
|
|
||||||
|
|
||||||
# Set initial position from the first point of the spline
|
|
||||||
initial_pos = self._spline_path[0]
|
|
||||||
self._pos_x_ft, self._pos_y_ft, self._pos_z_ft = initial_pos[0], initial_pos[1], initial_pos[2]
|
|
||||||
|
|
||||||
# Calculate total duration from waypoints
|
|
||||||
total_duration = sum(wp.duration_s or 0.0 for wp in self.trajectory if wp.duration_s)
|
|
||||||
|
|
||||||
# Create a time mapping for each point in the spline path
|
|
||||||
if self._spline_path is not None and len(self._spline_path) > 1:
|
|
||||||
self._spline_point_times = [i * (total_duration / (len(self._spline_path) - 1)) for i in range(len(self._spline_path))]
|
|
||||||
else:
|
|
||||||
self._spline_point_times = [0.0]
|
|
||||||
|
|
||||||
self._update_current_polar_coords()
|
|
||||||
|
|
||||||
|
for i, point in enumerate(splined_points):
|
||||||
|
time = (i / (len(splined_points) - 1)) * total_duration_s if len(splined_points) > 1 else 0.0
|
||||||
|
path.append((time, point[0], point[1], point[2]))
|
||||||
else:
|
else:
|
||||||
# Waypoint-based simulation reset
|
path = keyframes
|
||||||
if not self.trajectory or self.trajectory[0].maneuver_type != ManeuverType.FLY_TO_POINT:
|
|
||||||
self._pos_x_ft = self._pos_y_ft = self._pos_z_ft = 0.0
|
|
||||||
self.current_velocity_fps = 0.0
|
|
||||||
self.current_heading_deg = 0.0
|
|
||||||
self.current_pitch_deg = 0.0
|
|
||||||
else:
|
|
||||||
first_wp = self.trajectory[0]
|
|
||||||
self.current_range_nm = first_wp.target_range_nm or 0.0
|
|
||||||
self.current_azimuth_deg = first_wp.target_azimuth_deg or 0.0
|
|
||||||
self.current_altitude_ft = first_wp.target_altitude_ft or 0.0
|
|
||||||
self.current_velocity_fps = first_wp.target_velocity_fps or 0.0
|
|
||||||
self.current_heading_deg = first_wp.target_heading_deg or 0.0
|
|
||||||
self.current_pitch_deg = 0.0
|
|
||||||
|
|
||||||
range_ft = self.current_range_nm * NM_TO_FT
|
return path, total_duration_s
|
||||||
az_rad = math.radians(self.current_azimuth_deg)
|
|
||||||
self._pos_x_ft = range_ft * math.sin(az_rad)
|
|
||||||
self._pos_y_ft = range_ft * math.cos(az_rad)
|
|
||||||
self._pos_z_ft = self.current_altitude_ft
|
|
||||||
|
|
||||||
self._activate_next_waypoint()
|
|
||||||
|
|
||||||
self._update_current_polar_coords()
|
|
||||||
|
|
||||||
def _activate_next_waypoint(self):
|
|
||||||
"""Pre-calculates and activates the next waypoint in the trajectory."""
|
|
||||||
self._current_waypoint_index += 1
|
|
||||||
self._time_in_waypoint_s = 0.0
|
|
||||||
|
|
||||||
if self._current_waypoint_index >= len(self.trajectory):
|
|
||||||
self._current_waypoint_index = -1; return
|
|
||||||
|
|
||||||
wp = self.trajectory[self._current_waypoint_index]
|
|
||||||
wp._start_velocity_fps = self.current_velocity_fps
|
|
||||||
wp._start_heading_deg = self.current_heading_deg
|
|
||||||
wp._start_altitude_ft = self.current_altitude_ft
|
|
||||||
|
|
||||||
if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
|
|
||||||
target_range_ft = (wp.target_range_nm or 0.0) * NM_TO_FT
|
|
||||||
target_az_rad = math.radians(wp.target_azimuth_deg or 0.0)
|
|
||||||
target_x = target_range_ft * math.sin(target_az_rad)
|
|
||||||
target_y = target_range_ft * math.cos(target_az_rad)
|
|
||||||
target_z = wp.target_altitude_ft or self._pos_z_ft
|
|
||||||
|
|
||||||
dx, dy, dz = target_x - self._pos_x_ft, target_y - self._pos_y_ft, target_z - self._pos_z_ft
|
|
||||||
dist_3d = math.sqrt(dx**2 + dy**2 + dz**2)
|
|
||||||
dist_2d = math.sqrt(dx**2 + dy**2)
|
|
||||||
|
|
||||||
self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360
|
|
||||||
self.current_pitch_deg = math.degrees(math.atan2(dz, dist_2d)) if dist_2d > 1 else 90.0 if dz > 0 else -90.0
|
|
||||||
|
|
||||||
if wp.duration_s and wp.duration_s > 0:
|
|
||||||
self.current_velocity_fps = dist_3d / wp.duration_s
|
|
||||||
wp._calculated_duration_s = wp.duration_s
|
|
||||||
else:
|
|
||||||
self.current_velocity_fps = 0
|
|
||||||
wp._calculated_duration_s = 999999
|
|
||||||
elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
|
|
||||||
# Aggiorna la posizione iniziale per il prossimo segmento
|
|
||||||
heading_rad = math.radians(wp.target_heading_deg or self.current_heading_deg)
|
|
||||||
pitch_rad = math.radians(self.current_pitch_deg)
|
|
||||||
dist_moved = (wp.target_velocity_fps or self.current_velocity_fps) * (wp.duration_s or 0.0)
|
|
||||||
dist_2d = dist_moved * math.cos(pitch_rad)
|
|
||||||
self._pos_x_ft += dist_2d * math.sin(heading_rad)
|
|
||||||
self._pos_y_ft += dist_2d * math.cos(heading_rad)
|
|
||||||
self._pos_z_ft += dist_moved * math.sin(pitch_rad)
|
|
||||||
self._update_current_polar_coords()
|
|
||||||
self.current_heading_deg = wp.target_heading_deg or self.current_heading_deg
|
|
||||||
self.current_velocity_fps = wp.target_velocity_fps or self.current_velocity_fps
|
|
||||||
wp._calculated_duration_s = wp.duration_s or 0.0
|
|
||||||
else:
|
|
||||||
self.current_pitch_deg = 0
|
|
||||||
|
|
||||||
def update_state(self, delta_time_s: float):
|
def update_state(self, delta_time_s: float):
|
||||||
if not self.active:
|
# ... (questo metodo rimane invariato)
|
||||||
|
if not self.active or not self._path:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.use_spline and self._spline_path:
|
self._sim_time_s += delta_time_s
|
||||||
self._sim_time_s += delta_time_s
|
current_sim_time = min(self._sim_time_s, self._total_duration_s)
|
||||||
|
|
||||||
total_duration = self._spline_point_times[-1]
|
if current_sim_time >= self._total_duration_s:
|
||||||
if total_duration <= 0 or self._sim_time_s >= total_duration:
|
final_pos = self._path[-1]
|
||||||
# Simulation finished, stay at the last point
|
self._pos_x_ft, self._pos_y_ft, self._pos_z_ft = final_pos[1], final_pos[2], final_pos[3]
|
||||||
self._spline_index = len(self._spline_path) - 1
|
self.current_velocity_fps = 0.0
|
||||||
|
if self._sim_time_s >= self._total_duration_s:
|
||||||
self.active = False
|
self.active = False
|
||||||
self.current_velocity_fps = 0.0
|
else:
|
||||||
else:
|
p1 = self._path[0]
|
||||||
# Find the current index in the spline path based on time
|
p2 = self._path[-1]
|
||||||
progress = self._sim_time_s / total_duration
|
for i in range(len(self._path) - 1):
|
||||||
self._spline_index = int(progress * (len(self._spline_path) - 1))
|
if self._path[i][0] <= current_sim_time <= self._path[i+1][0]:
|
||||||
|
p1 = self._path[i]
|
||||||
|
p2 = self._path[i+1]
|
||||||
|
break
|
||||||
|
|
||||||
pt = self._spline_path[self._spline_index]
|
segment_duration = p2[0] - p1[0]
|
||||||
|
progress = (current_sim_time - p1[0]) / segment_duration if segment_duration > 0 else 1.0
|
||||||
|
|
||||||
# Determine previous point for calculating heading/pitch
|
prev_x, prev_y, prev_z = self._pos_x_ft, self._pos_y_ft, self._pos_z_ft
|
||||||
prev_pt = self._spline_path[self._spline_index - 1] if self._spline_index > 0 else pt
|
|
||||||
|
|
||||||
# Update position (path is already in feet)
|
self._pos_x_ft = p1[1] + (p2[1] - p1[1]) * progress
|
||||||
self._pos_x_ft, self._pos_y_ft, self._pos_z_ft = pt[0], pt[1], pt[2]
|
self._pos_y_ft = p1[2] + (p2[2] - p1[2]) * progress
|
||||||
|
self._pos_z_ft = p1[3] + (p2[3] - p1[3]) * progress
|
||||||
|
|
||||||
# Update kinematics
|
dx = self._pos_x_ft - prev_x
|
||||||
dx = self._pos_x_ft - prev_pt[0]
|
dy = self._pos_y_ft - prev_y
|
||||||
dy = self._pos_y_ft - prev_pt[1]
|
dz = self._pos_z_ft - prev_z
|
||||||
dz = self._pos_z_ft - prev_pt[2]
|
|
||||||
|
|
||||||
dist_moved_since_last_tick = math.sqrt(dx**2 + dy**2 + dz**2)
|
dist_moved_3d = math.sqrt(dx**2 + dy**2 + dz**2)
|
||||||
dist_2d = math.sqrt(dx**2 + dy**2)
|
dist_moved_2d = math.sqrt(dx**2 + dy**2)
|
||||||
|
|
||||||
if dist_2d > 0.1: # Avoid division by zero or noise
|
|
||||||
self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360
|
|
||||||
self.current_pitch_deg = math.degrees(math.atan2(dz, dist_2d))
|
|
||||||
|
|
||||||
if delta_time_s > 0:
|
if delta_time_s > 0:
|
||||||
self.current_velocity_fps = dist_moved_since_last_tick / delta_time_s
|
self.current_velocity_fps = dist_moved_3d / delta_time_s
|
||||||
|
|
||||||
self._update_current_polar_coords()
|
if dist_moved_2d > 0.1:
|
||||||
|
self.current_heading_deg = math.degrees(math.atan2(dx, dy)) % 360
|
||||||
|
self.current_pitch_deg = math.degrees(math.atan2(dz, dist_moved_2d))
|
||||||
|
|
||||||
# --- DEBUG LOG ---
|
self._update_current_polar_coords()
|
||||||
#import logging
|
|
||||||
#logging.getLogger("target_simulator.spline_debug").info(
|
|
||||||
# f"SplinePos: t={self._sim_time_s:.2f}s idx={self._spline_index} x={self._pos_x_ft:.2f} y={self._pos_y_ft:.2f} z={self._pos_z_ft:.2f}")
|
|
||||||
# --- DEBUG LOG ---
|
|
||||||
#import logging
|
|
||||||
#logging.getLogger("target_simulator.spline_debug").info(
|
|
||||||
# f"SplinePos: t={self._sim_time_s:.2f}s idx={self._spline_index} x={self._pos_x_ft:.2f} y={self._pos_y_ft:.2f} z={self._pos_z_ft:.2f}")
|
|
||||||
else:
|
|
||||||
# Logica classica
|
|
||||||
self._sim_time_s += delta_time_s
|
|
||||||
self._time_in_waypoint_s += delta_time_s
|
|
||||||
if self._current_waypoint_index == -1:
|
|
||||||
# Traiettoria terminata: disattiva target
|
|
||||||
self.active = False
|
|
||||||
self.current_velocity_fps = 0.0
|
|
||||||
return
|
|
||||||
wp = self.trajectory[self._current_waypoint_index]
|
|
||||||
duration = wp._calculated_duration_s if wp._calculated_duration_s is not None else wp.duration_s or 0.0
|
|
||||||
if self._time_in_waypoint_s >= duration:
|
|
||||||
# Finalize state to match waypoint target
|
|
||||||
if wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
|
|
||||||
self.current_velocity_fps = wp.target_velocity_fps or self.current_velocity_fps
|
|
||||||
self.current_heading_deg = wp.target_heading_deg or self.current_heading_deg
|
|
||||||
self.current_altitude_ft = wp.target_altitude_ft or self.current_altitude_ft
|
|
||||||
self._activate_next_waypoint()
|
|
||||||
if self._current_waypoint_index == -1:
|
|
||||||
# Traiettoria terminata: disattiva target
|
|
||||||
self.active = False
|
|
||||||
self.current_velocity_fps = 0.0
|
|
||||||
return
|
|
||||||
wp = self.trajectory[self._current_waypoint_index] # Get new active waypoint
|
|
||||||
duration = wp._calculated_duration_s if wp._calculated_duration_s is not None else wp.duration_s or 0.0
|
|
||||||
progress = self._time_in_waypoint_s / duration if duration > 0 else 1.0
|
|
||||||
if wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
|
|
||||||
# Interpolate kinematics during the maneuver
|
|
||||||
self.current_velocity_fps = wp._start_velocity_fps + ((wp.target_velocity_fps or wp._start_velocity_fps) - wp._start_velocity_fps) * progress
|
|
||||||
self.current_altitude_ft = wp._start_altitude_ft + ((wp.target_altitude_ft or wp._start_altitude_ft) - wp._start_altitude_ft) * progress
|
|
||||||
delta_heading = ((wp.target_heading_deg or wp._start_heading_deg) - wp._start_heading_deg + 180) % 360 - 180
|
|
||||||
self.current_heading_deg = (wp._start_heading_deg + delta_heading * progress) % 360
|
|
||||||
# Update position using current 3D vector
|
|
||||||
heading_rad = math.radians(self.current_heading_deg)
|
|
||||||
pitch_rad = math.radians(self.current_pitch_deg)
|
|
||||||
dist_moved = self.current_velocity_fps * delta_time_s
|
|
||||||
dist_2d = dist_moved * math.cos(pitch_rad)
|
|
||||||
self._pos_x_ft += dist_2d * math.sin(heading_rad)
|
|
||||||
self._pos_y_ft += dist_2d * math.cos(heading_rad)
|
|
||||||
self._pos_z_ft += dist_moved * math.sin(pitch_rad)
|
|
||||||
self._update_current_polar_coords()
|
|
||||||
|
|
||||||
def _update_current_polar_coords(self):
|
def _update_current_polar_coords(self):
|
||||||
"""Recalculates current polar coordinates from Cartesian ones."""
|
# ... (questo metodo rimane invariato)
|
||||||
self.current_range_nm = math.sqrt(self._pos_x_ft**2 + self._pos_y_ft**2) / NM_TO_FT
|
self.current_range_nm = math.sqrt(self._pos_x_ft**2 + self._pos_y_ft**2) / NM_TO_FT
|
||||||
self.current_azimuth_deg = math.degrees(math.atan2(self._pos_x_ft, self._pos_y_ft))
|
self.current_azimuth_deg = math.degrees(math.atan2(self._pos_x_ft, self._pos_y_ft)) % 360
|
||||||
self.current_altitude_ft = self._pos_z_ft
|
self.current_altitude_ft = self._pos_z_ft
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
def to_dict(self) -> Dict:
|
||||||
data = {
|
# ... (questo metodo rimane invariato)
|
||||||
|
return {
|
||||||
"target_id": self.target_id,
|
"target_id": self.target_id,
|
||||||
"active": self.active,
|
"active": self.active,
|
||||||
"traceable": self.traceable,
|
"traceable": self.traceable,
|
||||||
"trajectory": [wp.to_dict() for wp in self.trajectory],
|
"trajectory": [wp.to_dict() for wp in self.trajectory],
|
||||||
"use_spline": self.use_spline
|
"use_spline": self.use_spline
|
||||||
}
|
}
|
||||||
return data
|
|
||||||
|
|
||||||
class Scenario:
|
class Scenario:
|
||||||
def __init__(self, name: str = "Untitled Scenario"):
|
def __init__(self, name: str = "Untitled Scenario"):
|
||||||
@ -341,6 +299,10 @@ class Scenario:
|
|||||||
for target in self.targets.values():
|
for target in self.targets.values():
|
||||||
target.update_state(delta_time_s)
|
target.update_state(delta_time_s)
|
||||||
|
|
||||||
|
def is_finished(self) -> bool:
|
||||||
|
"""Checks if all targets in the scenario have finished their trajectory."""
|
||||||
|
return all(not target.active for target in self.targets.values())
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
def to_dict(self) -> Dict:
|
||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
|
|||||||
@ -20,8 +20,6 @@ TICK_RATE_HZ = 10.0
|
|||||||
TICK_INTERVAL_S = 1.0 / TICK_RATE_HZ
|
TICK_INTERVAL_S = 1.0 / TICK_RATE_HZ
|
||||||
|
|
||||||
class SimulationEngine(threading.Thread):
|
class SimulationEngine(threading.Thread):
|
||||||
# ...existing code...
|
|
||||||
|
|
||||||
def __init__(self, communicator: Optional[CommunicatorInterface], update_queue: Optional[Queue]):
|
def __init__(self, communicator: Optional[CommunicatorInterface], update_queue: Optional[Queue]):
|
||||||
super().__init__(daemon=True, name="SimulationEngineThread")
|
super().__init__(daemon=True, name="SimulationEngineThread")
|
||||||
self.logger = get_logger(__name__)
|
self.logger = get_logger(__name__)
|
||||||
@ -40,7 +38,7 @@ class SimulationEngine(threading.Thread):
|
|||||||
|
|
||||||
def load_scenario(self, scenario: Scenario):
|
def load_scenario(self, scenario: Scenario):
|
||||||
"""Loads a new scenario into the engine and resets its simulation state."""
|
"""Loads a new scenario into the engine and resets its simulation state."""
|
||||||
self.logger.info(f"Loading scenario '{scenario.name}' (target_{scenario.get_all_targets()[0].target_id if scenario.get_all_targets() else 'N/A'}) into simulation engine.")
|
self.logger.info(f"Loading scenario '{scenario.name}' into simulation engine.")
|
||||||
self.scenario = scenario
|
self.scenario = scenario
|
||||||
self.scenario.reset_simulation()
|
self.scenario.reset_simulation()
|
||||||
|
|
||||||
@ -65,6 +63,7 @@ class SimulationEngine(threading.Thread):
|
|||||||
if not self.scenario:
|
if not self.scenario:
|
||||||
time.sleep(TICK_INTERVAL_S)
|
time.sleep(TICK_INTERVAL_S)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._is_paused:
|
if self._is_paused:
|
||||||
time.sleep(TICK_INTERVAL_S)
|
time.sleep(TICK_INTERVAL_S)
|
||||||
continue
|
continue
|
||||||
@ -80,39 +79,36 @@ class SimulationEngine(threading.Thread):
|
|||||||
updated_targets = self.scenario.get_all_targets()
|
updated_targets = self.scenario.get_all_targets()
|
||||||
|
|
||||||
# --- Check for simulation end ---
|
# --- Check for simulation end ---
|
||||||
if hasattr(self.scenario, 'is_finished') and self.scenario.is_finished():
|
if self.scenario.is_finished():
|
||||||
self.logger.info("Simulation finished: auto-stopping thread.")
|
self.logger.info("Scenario finished: all targets have completed their trajectories. Stopping engine.")
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
if self.update_queue:
|
if self.update_queue:
|
||||||
try:
|
try:
|
||||||
self.update_queue.put_nowait('SIMULATION_FINISHED')
|
self.update_queue.put_nowait('SIMULATION_FINISHED')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass # Ignore if queue is full on the last message
|
||||||
break
|
break
|
||||||
|
|
||||||
# --- Communication Step (conditional) ---
|
# --- Communication Step (conditional) ---
|
||||||
if self.communicator and self.communicator.is_open:
|
if self.communicator and self.communicator.is_open:
|
||||||
if current_time - self._last_update_time >= self.update_interval_s:
|
if current_time - self._last_update_time >= self.update_interval_s:
|
||||||
self._last_update_time = current_time
|
self._last_update_time = current_time
|
||||||
|
commands_to_send = []
|
||||||
|
for target in updated_targets:
|
||||||
|
if target.active: # Only send updates for active targets
|
||||||
|
commands_to_send.append(command_builder.build_tgtset_from_target_state(target))
|
||||||
|
|
||||||
# --- Batch commands for communicators that support it (TFTP) ---
|
if commands_to_send:
|
||||||
if hasattr(self.communicator, 'send_commands'):
|
# Batch commands for communicators that support it (TFTP)
|
||||||
commands_to_send = []
|
if hasattr(self.communicator, 'send_commands'):
|
||||||
for target in updated_targets:
|
|
||||||
if target.active:
|
|
||||||
commands_to_send.append(command_builder.build_tgtset_from_target_state(target))
|
|
||||||
if commands_to_send:
|
|
||||||
self.communicator.send_commands(commands_to_send)
|
self.communicator.send_commands(commands_to_send)
|
||||||
# --- Send commands individually for other communicators (Serial) ---
|
# Send commands individually for others (Serial)
|
||||||
else:
|
else:
|
||||||
for target in updated_targets:
|
for command in commands_to_send:
|
||||||
if not target.active:
|
if hasattr(self.communicator, 'send_command'):
|
||||||
continue
|
self.communicator.send_command(command)
|
||||||
command = command_builder.build_tgtset_from_target_state(target)
|
elif hasattr(self.communicator, '_send_single_command'):
|
||||||
if hasattr(self.communicator, 'send_command'):
|
self.communicator._send_single_command(command)
|
||||||
self.communicator.send_command(command)
|
|
||||||
elif hasattr(self.communicator, '_send_single_command'):
|
|
||||||
self.communicator._send_single_command(command)
|
|
||||||
|
|
||||||
# --- GUI Update Step (conditional) ---
|
# --- GUI Update Step (conditional) ---
|
||||||
if self.update_queue:
|
if self.update_queue:
|
||||||
@ -124,6 +120,9 @@ class SimulationEngine(threading.Thread):
|
|||||||
# --- Loop Control ---
|
# --- Loop Control ---
|
||||||
time.sleep(TICK_INTERVAL_S)
|
time.sleep(TICK_INTERVAL_S)
|
||||||
|
|
||||||
|
self._is_running_event.clear()
|
||||||
|
self.logger.info("Simulation engine thread stopped.")
|
||||||
|
|
||||||
def pause(self):
|
def pause(self):
|
||||||
"""Pauses the simulation loop."""
|
"""Pauses the simulation loop."""
|
||||||
self.logger.info("Simulation paused.")
|
self.logger.info("Simulation paused.")
|
||||||
@ -134,9 +133,6 @@ class SimulationEngine(threading.Thread):
|
|||||||
self.logger.info("Simulation resumed.")
|
self.logger.info("Simulation resumed.")
|
||||||
self._is_paused = False
|
self._is_paused = False
|
||||||
|
|
||||||
self._is_running_event.clear()
|
|
||||||
self.logger.info("Simulation engine thread stopped.")
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Signals the simulation thread to stop and waits for it to terminate."""
|
"""Signals the simulation thread to stop and waits for it to terminate."""
|
||||||
self.logger.info("Stop signal received for simulation thread.")
|
self.logger.info("Stop signal received for simulation thread.")
|
||||||
|
|||||||
@ -9,24 +9,14 @@ import tkinter as tk
|
|||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
import math
|
import math
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import copy
|
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
from target_simulator.core.models import NM_TO_FT
|
|
||||||
|
|
||||||
# Use absolute imports
|
# Use absolute imports
|
||||||
from target_simulator.core.models import Target, Waypoint, ManeuverType
|
from target_simulator.core.models import Target, Waypoint, ManeuverType, NM_TO_FT
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
class PPIDisplay(ttk.Frame):
|
class PPIDisplay(ttk.Frame):
|
||||||
def clear_previews(self):
|
|
||||||
"""Clears all preview artists from the plot."""
|
|
||||||
for artist in self.preview_artists:
|
|
||||||
artist.set_data([], [])
|
|
||||||
self.canvas.draw()
|
|
||||||
def _on_motion(self, event):
|
|
||||||
# Stub: implementazione futura per tooltip/mouseover
|
|
||||||
pass
|
|
||||||
"""A custom, reusable widget for the PPI radar display."""
|
"""A custom, reusable widget for the PPI radar display."""
|
||||||
|
|
||||||
def __init__(self, master, max_range_nm: int = 100, scan_limit_deg: int = 60):
|
def __init__(self, master, max_range_nm: int = 100, scan_limit_deg: int = 60):
|
||||||
@ -34,9 +24,11 @@ class PPIDisplay(ttk.Frame):
|
|||||||
self.max_range = max_range_nm
|
self.max_range = max_range_nm
|
||||||
self.scan_limit_deg = scan_limit_deg
|
self.scan_limit_deg = scan_limit_deg
|
||||||
|
|
||||||
|
# Artists for dynamic target display
|
||||||
self.target_artists = []
|
self.target_artists = []
|
||||||
self.active_targets: List[Target] = []
|
self.active_targets: List[Target] = []
|
||||||
self._target_dots = []
|
|
||||||
|
# Artists for trajectory preview display
|
||||||
self.preview_artists = []
|
self.preview_artists = []
|
||||||
|
|
||||||
self._create_controls()
|
self._create_controls()
|
||||||
@ -49,20 +41,13 @@ class PPIDisplay(ttk.Frame):
|
|||||||
|
|
||||||
ttk.Label(self.controls_frame, text="Range (NM):").pack(side=tk.LEFT)
|
ttk.Label(self.controls_frame, text="Range (NM):").pack(side=tk.LEFT)
|
||||||
|
|
||||||
# Create a list of valid range steps up to the theoretical max_range
|
all_steps = [10, 20, 40, 80, 100, 160, 240, 320]
|
||||||
all_steps = [10, 20, 40, 80, 100, 160]
|
|
||||||
valid_steps = sorted([s for s in all_steps if s <= self.max_range])
|
valid_steps = sorted([s for s in all_steps if s <= self.max_range])
|
||||||
if not valid_steps:
|
if not valid_steps or self.max_range not in valid_steps:
|
||||||
valid_steps = [self.max_range]
|
|
||||||
|
|
||||||
# Ensure the initial max range is in the list if not a standard step
|
|
||||||
if self.max_range not in valid_steps:
|
|
||||||
valid_steps.append(self.max_range)
|
valid_steps.append(self.max_range)
|
||||||
valid_steps.sort()
|
valid_steps.sort()
|
||||||
|
|
||||||
# The initial value for the combobox is the max_range passed to the constructor
|
|
||||||
self.range_var = tk.IntVar(value=self.max_range)
|
self.range_var = tk.IntVar(value=self.max_range)
|
||||||
|
|
||||||
self.range_selector = ttk.Combobox(
|
self.range_selector = ttk.Combobox(
|
||||||
self.controls_frame, textvariable=self.range_var,
|
self.controls_frame, textvariable=self.range_var,
|
||||||
values=valid_steps, state="readonly", width=5
|
values=valid_steps, state="readonly", width=5
|
||||||
@ -73,7 +58,7 @@ class PPIDisplay(ttk.Frame):
|
|||||||
def _create_plot(self):
|
def _create_plot(self):
|
||||||
"""Initializes the Matplotlib polar plot."""
|
"""Initializes the Matplotlib polar plot."""
|
||||||
fig = Figure(figsize=(5, 5), dpi=100, facecolor='#3E3E3E')
|
fig = Figure(figsize=(5, 5), dpi=100, facecolor='#3E3E3E')
|
||||||
fig.subplots_adjust(left=0.05, right=0.95, top=0.85, bottom=0.05)
|
fig.subplots_adjust(left=0.05, right=0.95, top=0.9, bottom=0.05)
|
||||||
|
|
||||||
self.ax = fig.add_subplot(111, projection='polar', facecolor='#2E2E2E')
|
self.ax = fig.add_subplot(111, projection='polar', facecolor='#2E2E2E')
|
||||||
|
|
||||||
@ -83,302 +68,135 @@ class PPIDisplay(ttk.Frame):
|
|||||||
self.ax.set_ylim(0, self.range_var.get())
|
self.ax.set_ylim(0, self.range_var.get())
|
||||||
|
|
||||||
angles = np.arange(0, 360, 30)
|
angles = np.arange(0, 360, 30)
|
||||||
labels = [f"{angle}°" if angle == 0 else f"+{angle}°" if angle < 180 else "±180°" if angle == 180 else f"-{360 - angle}°" for angle in angles]
|
labels = [f"{angle}°" for angle in angles]
|
||||||
self.ax.set_thetagrids(angles, labels)
|
self.ax.set_thetagrids(angles, labels)
|
||||||
|
|
||||||
self.ax.tick_params(axis='x', colors='white', labelsize=8)
|
self.ax.tick_params(axis='x', colors='white', labelsize=8)
|
||||||
self.ax.tick_params(axis='y', colors='white', labelsize=8)
|
self.ax.tick_params(axis='y', colors='white', labelsize=8)
|
||||||
self.ax.grid(color='white', linestyle='--', linewidth=0.5, alpha=0.5)
|
self.ax.grid(color='white', linestyle='--', linewidth=0.5, alpha=0.5)
|
||||||
self.ax.spines['polar'].set_color('white')
|
self.ax.spines['polar'].set_color('white')
|
||||||
self.ax.set_title("PPI Display", color='white', y=1.08)
|
self.ax.set_title("PPI Display", color='white')
|
||||||
|
|
||||||
# --- Artists for drawing ---
|
# --- Artists for drawing previews ---
|
||||||
self._start_plot, = self.ax.plot([], [], 'go', markersize=8)
|
self._path_plot, = self.ax.plot([], [], 'g--', linewidth=1.5, label="Path")
|
||||||
self._waypoints_plot, = self.ax.plot([], [], 'y+', markersize=10, mew=2, linestyle='None')
|
self._start_plot, = self.ax.plot([], [], 'go', markersize=8, label="Start")
|
||||||
self._path_plot, = self.ax.plot([], [], 'g--', linewidth=1.5)
|
self._waypoints_plot, = self.ax.plot([], [], 'y+', markersize=10, mew=2, label="Waypoints")
|
||||||
self._preview_artist, = self.ax.plot([], [], color="orange", linestyle="--", linewidth=2, alpha=0.7)
|
self.preview_artists = [self._path_plot, self._start_plot, self._waypoints_plot]
|
||||||
self._heading_artist, = self.ax.plot([], [], color='red', linewidth=1, alpha=0.8)
|
|
||||||
self.preview_artists = [self._start_plot, self._waypoints_plot, self._path_plot, self._preview_artist, self._heading_artist]
|
|
||||||
|
|
||||||
# --- NEW: Create artists for scan lines ---
|
# --- Artists for scan lines ---
|
||||||
limit_rad = np.deg2rad(self.scan_limit_deg)
|
limit_rad = np.deg2rad(self.scan_limit_deg)
|
||||||
self._scan_line_1, = self.ax.plot([limit_rad, limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
self._scan_line_1, = self.ax.plot([limit_rad, limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
||||||
self._scan_line_2, = self.ax.plot([-limit_rad, -limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
self._scan_line_2, = self.ax.plot([-limit_rad, -limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
||||||
|
|
||||||
self._tooltip_label = None
|
|
||||||
|
|
||||||
self.canvas = FigureCanvasTkAgg(fig, master=self)
|
self.canvas = FigureCanvasTkAgg(fig, master=self)
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||||
self.canvas.mpl_connect('motion_notify_event', self._on_motion)
|
|
||||||
|
|
||||||
# --- NEW: Initial draw of scan lines ---
|
|
||||||
self._update_scan_lines()
|
self._update_scan_lines()
|
||||||
|
|
||||||
def _update_scan_lines(self):
|
def _update_scan_lines(self):
|
||||||
"""Updates the length of the scan sector lines to match the current range."""
|
"""Updates the length and position of the scan sector lines."""
|
||||||
current_range_max = self.ax.get_ylim()[1]
|
current_range_max = self.ax.get_ylim()[1]
|
||||||
self._scan_line_1.set_ydata([0, current_range_max])
|
limit_rad = np.deg2rad(self.scan_limit_deg)
|
||||||
self._scan_line_2.set_ydata([0, current_range_max])
|
|
||||||
|
self._scan_line_1.set_data([limit_rad, limit_rad], [0, current_range_max])
|
||||||
|
self._scan_line_2.set_data([-limit_rad, -limit_rad], [0, current_range_max])
|
||||||
|
|
||||||
def _on_range_selected(self, event=None):
|
def _on_range_selected(self, event=None):
|
||||||
"""Handles the selection of a new range."""
|
"""Handles the selection of a new range."""
|
||||||
new_range = self.range_var.get()
|
new_range = self.range_var.get()
|
||||||
self.ax.set_ylim(0, new_range)
|
self.ax.set_ylim(0, new_range)
|
||||||
|
|
||||||
# --- NEW: Update scan lines on zoom change ---
|
|
||||||
self._update_scan_lines()
|
self._update_scan_lines()
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
self.update_targets(self.active_targets)
|
def clear_previews(self):
|
||||||
self.canvas.draw()
|
"""Clears all preview-related artists from the plot."""
|
||||||
|
for artist in self.preview_artists:
|
||||||
# ... il resto dei metodi rimane invariato ...
|
artist.set_data([], [])
|
||||||
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
def update_targets(self, targets: List[Target]):
|
def update_targets(self, targets: List[Target]):
|
||||||
# (This method is unchanged)
|
"""Updates the display with the current state of active targets."""
|
||||||
self.active_targets = [t for t in targets if t.active]
|
self.active_targets = [t for t in targets if t.active]
|
||||||
for artist in self.target_artists: artist.remove()
|
|
||||||
|
# Clear previous target artists
|
||||||
|
for artist in self.target_artists:
|
||||||
|
artist.remove()
|
||||||
self.target_artists.clear()
|
self.target_artists.clear()
|
||||||
self._target_dots.clear()
|
|
||||||
vector_len = self.range_var.get() / 25
|
vector_len_nm = self.range_var.get() / 20.0 # Length of heading vector
|
||||||
|
|
||||||
for target in self.active_targets:
|
for target in self.active_targets:
|
||||||
r = target.current_range_nm
|
# Target's polar coordinates
|
||||||
theta = np.deg2rad(target.current_azimuth_deg)
|
r_nm = target.current_range_nm
|
||||||
dot, = self.ax.plot(theta, r, 'o', markersize=5, color='red')
|
theta_rad = np.deg2rad(target.current_azimuth_deg)
|
||||||
|
|
||||||
|
# Draw the target dot
|
||||||
|
dot, = self.ax.plot(theta_rad, r_nm, 'o', markersize=6, color='red')
|
||||||
self.target_artists.append(dot)
|
self.target_artists.append(dot)
|
||||||
self._target_dots.append((dot, target))
|
|
||||||
x1, y1 = r * np.sin(theta), r * np.cos(theta)
|
# Draw the heading vector
|
||||||
h_rad = np.deg2rad(target.current_heading_deg)
|
heading_rad = np.deg2rad(target.current_heading_deg)
|
||||||
dx, dy = vector_len * np.sin(h_rad), vector_len * np.cos(h_rad)
|
|
||||||
x2, y2 = x1 + dx, y1 + dy
|
# Convert to Cartesian to calculate endpoint, then back to polar
|
||||||
r2, th2 = np.sqrt(x2**2 + y2**2), np.arctan2(x2, y2)
|
x_nm = r_nm * np.sin(theta_rad)
|
||||||
line, = self.ax.plot([theta, th2], [r, r2], color='red', linewidth=1.2)
|
y_nm = r_nm * np.cos(theta_rad)
|
||||||
|
dx_nm = vector_len_nm * np.sin(heading_rad)
|
||||||
|
dy_nm = vector_len_nm * np.cos(heading_rad)
|
||||||
|
|
||||||
|
r2_nm = math.sqrt((x_nm + dx_nm)**2 + (y_nm + dy_nm)**2)
|
||||||
|
theta2_rad = math.atan2(x_nm + dx_nm, y_nm + dy_nm)
|
||||||
|
|
||||||
|
line, = self.ax.plot([theta_rad, theta2_rad], [r_nm, r2_nm], color='red', linewidth=1.2)
|
||||||
self.target_artists.append(line)
|
self.target_artists.append(line)
|
||||||
self.canvas.draw()
|
|
||||||
|
|
||||||
def draw_trajectory_preview(self, trajectory):
|
self.canvas.draw_idle()
|
||||||
|
|
||||||
|
def draw_trajectory_preview(self, waypoints: List[Waypoint], use_spline: bool):
|
||||||
"""
|
"""
|
||||||
Simulates and draws a trajectory preview.
|
Simulates and draws a trajectory preview by leveraging the static path generator.
|
||||||
Accepts either a list of Waypoint or a Target object.
|
|
||||||
"""
|
"""
|
||||||
# Pulisci tutto prima di ridisegnare
|
|
||||||
self.clear_previews()
|
self.clear_previews()
|
||||||
self._waypoints_plot.set_data([], [])
|
|
||||||
self._start_plot.set_data([], [])
|
|
||||||
if hasattr(self, '_preview_artist'):
|
|
||||||
self._preview_artist.set_data([], [])
|
|
||||||
# Rimuovi eventuali artisti extra creati manualmente (puntini/linee)
|
|
||||||
if hasattr(self, '_preview_extra_artists'):
|
|
||||||
for a in self._preview_extra_artists:
|
|
||||||
try:
|
|
||||||
a.remove()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._preview_extra_artists.clear()
|
|
||||||
else:
|
|
||||||
self._preview_extra_artists = []
|
|
||||||
# Forza la pulizia della canvas
|
|
||||||
self.canvas.draw()
|
|
||||||
self._on_range_selected()
|
|
||||||
|
|
||||||
# Determine input type
|
if not waypoints or waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
|
||||||
if isinstance(trajectory, list):
|
return
|
||||||
self._spline_preview_active = False
|
|
||||||
waypoints = trajectory
|
# Use the static method to get the path without creating a Target instance
|
||||||
if not waypoints:
|
path, _ = Target.generate_path_from_waypoints(waypoints, use_spline)
|
||||||
self.canvas.draw()
|
|
||||||
return
|
if not path:
|
||||||
if all(getattr(wp, 'maneuver_type', None) != ManeuverType.FLY_TO_POINT for wp in waypoints):
|
return
|
||||||
self.canvas.draw()
|
|
||||||
return
|
# --- Draw the main path (splined or polygonal) ---
|
||||||
# Classic preview (polyline) SOLO se spline non attiva
|
path_thetas = []
|
||||||
# (la preview spline cancella la classica)
|
path_rs = []
|
||||||
# Costruisci la lista dei punti da visualizzare
|
for point in path:
|
||||||
points = []
|
_time, x_ft, y_ft, _z_ft = point
|
||||||
# Stato corrente: range/azimuth
|
r_ft = math.sqrt(x_ft**2 + y_ft**2)
|
||||||
curr_r = None
|
theta_rad = math.atan2(x_ft, y_ft)
|
||||||
curr_theta = None
|
path_rs.append(r_ft / NM_TO_FT)
|
||||||
for i, wp in enumerate(waypoints):
|
path_thetas.append(theta_rad)
|
||||||
if getattr(wp, 'maneuver_type', None) == ManeuverType.FLY_TO_POINT:
|
|
||||||
curr_r = getattr(wp, 'target_range_nm', 0)
|
self._path_plot.set_data(path_thetas, path_rs)
|
||||||
curr_theta = math.radians(getattr(wp, 'target_azimuth_deg', 0))
|
|
||||||
points.append((curr_theta, curr_r, wp))
|
# --- Draw waypoint markers (only for Fly to Point) ---
|
||||||
elif getattr(wp, 'maneuver_type', None) == ManeuverType.FLY_FOR_DURATION:
|
wp_thetas = []
|
||||||
# Se non c'è punto iniziale, ignora
|
wp_rs = []
|
||||||
if curr_r is None or curr_theta is None:
|
for wp in waypoints:
|
||||||
continue
|
if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
|
||||||
vel_fps = getattr(wp, 'target_velocity_fps', 0)
|
r_nm = wp.target_range_nm or 0.0
|
||||||
vel_nmps = vel_fps / NM_TO_FT if vel_fps else 0
|
theta_rad = math.radians(wp.target_azimuth_deg or 0.0)
|
||||||
duration = getattr(wp, 'duration_s', 0)
|
wp_rs.append(r_nm)
|
||||||
heading_deg = getattr(wp, 'target_heading_deg', 0)
|
wp_thetas.append(theta_rad)
|
||||||
heading_rad = math.radians(heading_deg)
|
|
||||||
# Calcola delta x/y in coordinate polari
|
self._waypoints_plot.set_data(wp_thetas, wp_rs)
|
||||||
dr = vel_nmps * duration
|
|
||||||
theta1 = curr_theta + heading_rad
|
# --- Draw the start point ---
|
||||||
r1 = curr_r + dr
|
start_wp = waypoints[0]
|
||||||
curr_r = r1
|
start_r = start_wp.target_range_nm or 0.0
|
||||||
curr_theta = theta1
|
start_theta = math.radians(start_wp.target_azimuth_deg or 0.0)
|
||||||
points.append((curr_theta, curr_r, wp))
|
self._start_plot.set_data([start_theta], [start_r])
|
||||||
thetas = [p[0] for p in points]
|
self.canvas.draw_idle()
|
||||||
rs = [p[1] for p in points]
|
|
||||||
if len(thetas) == 1:
|
|
||||||
# Mostra solo il punto iniziale con stile simulazione
|
|
||||||
self._preview_artist.set_data([], [])
|
|
||||||
self._waypoints_plot.set_data([], [])
|
|
||||||
self._start_plot.set_data([], [])
|
|
||||||
wp0 = waypoints[0]
|
|
||||||
start_theta = thetas[0]
|
|
||||||
start_r = rs[0]
|
|
||||||
self._preview_extra_artists = []
|
|
||||||
# Puntino rosso
|
|
||||||
dot, = self.ax.plot([start_theta], [start_r], 'o', markersize=5, color='red')
|
|
||||||
self._preview_extra_artists.append(dot)
|
|
||||||
# Linea di heading corta
|
|
||||||
heading_deg = getattr(wp0, 'target_heading_deg', None)
|
|
||||||
if heading_deg is not None:
|
|
||||||
h_rad = math.radians(heading_deg)
|
|
||||||
vector_len = self.max_range / 25
|
|
||||||
x1, y1 = start_r * math.sin(start_theta), start_r * math.cos(start_theta)
|
|
||||||
dx, dy = vector_len * math.sin(h_rad), vector_len * math.cos(h_rad)
|
|
||||||
x2, y2 = x1 + dx, y1 + dy
|
|
||||||
r2, th2 = math.sqrt(x2**2 + y2**2), math.atan2(x2, y2)
|
|
||||||
line, = self.ax.plot([start_theta, th2], [start_r, r2], color='red', linewidth=1.2)
|
|
||||||
self._preview_extra_artists.append(line)
|
|
||||||
self.canvas.draw()
|
|
||||||
return
|
|
||||||
if len(thetas) < 2:
|
|
||||||
self.canvas.draw()
|
|
||||||
return
|
|
||||||
# Verifica se la preview è per spline: se sì, non disegnare la linea classica
|
|
||||||
if hasattr(self, '_spline_preview_active') and self._spline_preview_active:
|
|
||||||
self._preview_artist.set_data([], [])
|
|
||||||
else:
|
|
||||||
self._preview_artist.set_data(thetas, rs)
|
|
||||||
self._waypoints_plot.set_data(thetas, rs)
|
|
||||||
start_theta = thetas[0]
|
|
||||||
start_r = rs[0]
|
|
||||||
self._start_plot.set_data([start_theta], [start_r])
|
|
||||||
# Heading dal primo waypoint
|
|
||||||
wp0 = points[0][2]
|
|
||||||
heading_deg = getattr(wp0, 'target_heading_deg', None)
|
|
||||||
if heading_deg is not None:
|
|
||||||
h_rad = math.radians(heading_deg)
|
|
||||||
vector_len = self.max_range / 25
|
|
||||||
x1, y1 = start_r * math.sin(start_theta), start_r * math.cos(start_theta)
|
|
||||||
dx, dy = vector_len * math.sin(h_rad), vector_len * math.cos(h_rad)
|
|
||||||
x2, y2 = x1 + dx, y1 + dy
|
|
||||||
r2, th2 = math.sqrt(x2**2 + y2**2), math.atan2(x2, y2)
|
|
||||||
if hasattr(self, '_heading_artist'):
|
|
||||||
self._heading_artist.remove()
|
|
||||||
self._heading_artist, = self.ax.plot([start_theta, th2], [start_r, r2], color='red', linewidth=1.2)
|
|
||||||
# Heading per ogni punto finale FLY_FOR_DURATION
|
|
||||||
for i, (theta, r, wp) in enumerate(points):
|
|
||||||
if i == 0:
|
|
||||||
continue
|
|
||||||
if getattr(wp, 'maneuver_type', None) == ManeuverType.FLY_FOR_DURATION:
|
|
||||||
heading_deg = getattr(wp, 'target_heading_deg', None)
|
|
||||||
if heading_deg is not None:
|
|
||||||
h_rad = math.radians(heading_deg)
|
|
||||||
vector_len = self.max_range / 25
|
|
||||||
x1, y1 = r * math.sin(theta), r * math.cos(theta)
|
|
||||||
dx, dy = vector_len * math.sin(h_rad), vector_len * math.cos(h_rad)
|
|
||||||
x2, y2 = x1 + dx, y1 + dy
|
|
||||||
r2, th2 = math.sqrt(x2**2 + y2**2), math.atan2(x2, y2)
|
|
||||||
self.ax.plot([theta, th2], [r, r2], color='red', linewidth=1.2)
|
|
||||||
else:
|
|
||||||
# Assume Target object with trajectory and use_spline
|
|
||||||
waypoints = getattr(trajectory, "trajectory", [])
|
|
||||||
if len(waypoints) < 2:
|
|
||||||
self.canvas.draw()
|
|
||||||
return
|
|
||||||
if getattr(trajectory, "use_spline", False):
|
|
||||||
self._spline_preview_active = True
|
|
||||||
from target_simulator.utils.spline import catmull_rom_spline
|
|
||||||
# Costruisci la lista dei punti: waypoint espliciti + punti finali calcolati per FLY_FOR_DURATION
|
|
||||||
points = []
|
|
||||||
curr_r = None
|
|
||||||
curr_theta = None
|
|
||||||
for i, wp in enumerate(waypoints):
|
|
||||||
if getattr(wp, 'maneuver_type', None) == ManeuverType.FLY_TO_POINT:
|
|
||||||
curr_r = getattr(wp, 'target_range_nm', 0)
|
|
||||||
curr_theta = math.radians(getattr(wp, 'target_azimuth_deg', 0))
|
|
||||||
x_nm = curr_r * math.sin(curr_theta)
|
|
||||||
y_nm = curr_r * math.cos(curr_theta)
|
|
||||||
points.append((x_nm, y_nm))
|
|
||||||
elif getattr(wp, 'maneuver_type', None) == ManeuverType.FLY_FOR_DURATION:
|
|
||||||
# Se non c'è punto iniziale, ignora
|
|
||||||
if curr_r is None or curr_theta is None:
|
|
||||||
continue
|
|
||||||
vel_fps = getattr(wp, 'target_velocity_fps', 0)
|
|
||||||
vel_nmps = vel_fps / NM_TO_FT if vel_fps else 0
|
|
||||||
duration = getattr(wp, 'duration_s', 0)
|
|
||||||
heading_deg = getattr(wp, 'target_heading_deg', 0)
|
|
||||||
heading_rad = math.radians(heading_deg)
|
|
||||||
dr = vel_nmps * duration
|
|
||||||
theta1 = curr_theta + heading_rad
|
|
||||||
r1 = curr_r + dr
|
|
||||||
x_nm = r1 * math.sin(theta1)
|
|
||||||
y_nm = r1 * math.cos(theta1)
|
|
||||||
points.append((x_nm, y_nm))
|
|
||||||
curr_r = r1
|
|
||||||
curr_theta = theta1
|
|
||||||
if len(points) < 4:
|
|
||||||
spline_pts = points
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
spline_pts = catmull_rom_spline(points)
|
|
||||||
# DEBUG: salva i punti spline in un file CSV
|
|
||||||
import csv, os
|
|
||||||
temp_path = os.path.join(os.path.dirname(__file__), '..', '..', 'Temp', 'spline_preview.csv')
|
|
||||||
with open(temp_path, 'w', newline='') as csvfile:
|
|
||||||
writer = csv.writer(csvfile)
|
|
||||||
writer.writerow(['x_nm', 'y_nm'])
|
|
||||||
for pt in spline_pts:
|
|
||||||
writer.writerow([pt[0], pt[1]])
|
|
||||||
except Exception:
|
|
||||||
spline_pts = points
|
|
||||||
# Converte i punti spline da x/y a (theta, r) in NM
|
|
||||||
spline_thetas = []
|
|
||||||
spline_rs = []
|
|
||||||
for x_nm, y_nm in spline_pts:
|
|
||||||
r_nm = (x_nm**2 + y_nm**2)**0.5
|
|
||||||
theta = math.atan2(x_nm, y_nm)
|
|
||||||
spline_thetas.append(theta)
|
|
||||||
spline_rs.append(r_nm)
|
|
||||||
self._preview_artist.set_data(spline_thetas, spline_rs)
|
|
||||||
wp_thetas = []
|
|
||||||
wp_rs = []
|
|
||||||
for x_nm, y_nm in points:
|
|
||||||
r_nm = (x_nm**2 + y_nm**2)**0.5
|
|
||||||
theta = math.atan2(x_nm, y_nm)
|
|
||||||
wp_thetas.append(theta)
|
|
||||||
wp_rs.append(r_nm)
|
|
||||||
self._waypoints_plot.set_data(wp_thetas, wp_rs)
|
|
||||||
start_x_nm, start_y_nm = points[0]
|
|
||||||
start_r_nm = (start_x_nm**2 + start_y_nm**2)**0.5
|
|
||||||
start_theta = math.atan2(start_x_nm, start_y_nm)
|
|
||||||
self._start_plot.set_data([start_theta], [start_r_nm])
|
|
||||||
if hasattr(self, '_heading_artist'):
|
|
||||||
self._heading_artist.remove()
|
|
||||||
if len(points) > 1:
|
|
||||||
dx = points[1][0] - points[0][0]
|
|
||||||
dy = points[1][1] - points[0][1]
|
|
||||||
norm = (dx**2 + dy**2)**0.5
|
|
||||||
if norm > 0:
|
|
||||||
heading_len = 3
|
|
||||||
hx_nm = start_x_nm + heading_len * dx / norm
|
|
||||||
hy_nm = start_y_nm + heading_len * dy / norm
|
|
||||||
h_r_nm = (hx_nm**2 + hy_nm**2)**0.5
|
|
||||||
h_theta = math.atan2(hx_nm, hy_nm)
|
|
||||||
self._heading_artist, = self.ax.plot([start_theta, h_theta], [start_r_nm, h_r_nm], color='red', linewidth=1, alpha=0.8)
|
|
||||||
else:
|
|
||||||
self._spline_preview_active = False
|
|
||||||
xs = [wp.x for wp in waypoints]
|
|
||||||
ys = [wp.y for wp in waypoints]
|
|
||||||
self._preview_artist, = self.ax.plot(xs, ys, color="orange", linestyle="--", linewidth=2, alpha=0.7)
|
|
||||||
# ...rimosso blocco duplicato/non valido...
|
|
||||||
self._update_scan_lines()
|
|
||||||
self.canvas.draw()
|
|
||||||
|
|
||||||
def reconfigure_radar(self, max_range_nm: int, scan_limit_deg: int):
|
def reconfigure_radar(self, max_range_nm: int, scan_limit_deg: int):
|
||||||
"""
|
"""
|
||||||
@ -388,20 +206,20 @@ class PPIDisplay(ttk.Frame):
|
|||||||
self.scan_limit_deg = scan_limit_deg
|
self.scan_limit_deg = scan_limit_deg
|
||||||
|
|
||||||
# Update the range combobox values
|
# Update the range combobox values
|
||||||
steps = [10, 20, 40, 80, 100, 160]
|
steps = [10, 20, 40, 80, 100, 160, 240, 320]
|
||||||
valid_steps = sorted([s for s in steps if s <= self.max_range])
|
valid_steps = sorted([s for s in steps if s <= self.max_range])
|
||||||
if not valid_steps: valid_steps = [self.max_range]
|
if not valid_steps or self.max_range not in valid_steps:
|
||||||
if self.max_range not in valid_steps:
|
|
||||||
valid_steps.append(self.max_range)
|
valid_steps.append(self.max_range)
|
||||||
valid_steps.sort()
|
valid_steps.sort()
|
||||||
|
|
||||||
|
current_range = self.range_var.get()
|
||||||
self.range_selector['values'] = valid_steps
|
self.range_selector['values'] = valid_steps
|
||||||
self.range_var.set(self.max_range) # Set to the new max range
|
|
||||||
|
|
||||||
# Update the scan limit lines
|
# If the current range is still valid, keep it. Otherwise, reset to max.
|
||||||
limit_rad = np.deg2rad(self.scan_limit_deg)
|
if current_range in valid_steps:
|
||||||
self._scan_line_1.set_xdata([limit_rad, limit_rad])
|
self.range_var.set(current_range)
|
||||||
self._scan_line_2.set_xdata([-limit_rad, -limit_rad])
|
else:
|
||||||
|
self.range_var.set(self.max_range)
|
||||||
|
|
||||||
# Apply the new range and redraw everything
|
# Apply the new range and redraw everything
|
||||||
self._on_range_selected()
|
self._on_range_selected()
|
||||||
@ -1,15 +1,17 @@
|
|||||||
|
# target_simulator/gui/trajectory_editor_window.py
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox
|
||||||
import math
|
import math
|
||||||
from typing import List, Optional
|
from typing import List, Optional, cast
|
||||||
import copy
|
import copy
|
||||||
from queue import Queue, Empty
|
from queue import Queue
|
||||||
|
|
||||||
# Use absolute imports
|
# Use absolute imports
|
||||||
from target_simulator.core.models import Target, Waypoint, ManeuverType, MIN_TARGET_ID, MAX_TARGET_ID, FPS_TO_KNOTS, Scenario
|
from target_simulator.core.models import Target, Waypoint, ManeuverType, MIN_TARGET_ID, MAX_TARGET_ID, FPS_TO_KNOTS, Scenario
|
||||||
from target_simulator.gui.ppi_display import PPIDisplay
|
from target_simulator.gui.ppi_display import PPIDisplay
|
||||||
from target_simulator.gui.waypoint_editor_window import WaypointEditorWindow
|
from target_simulator.gui.waypoint_editor_window import WaypointEditorWindow
|
||||||
from target_simulator.core.simulation_engine import SimulationEngine
|
from target_simulator.core.simulation_engine import SimulationEngine, TICK_INTERVAL_S
|
||||||
|
|
||||||
GUI_QUEUE_POLL_INTERVAL_MS = 50
|
GUI_QUEUE_POLL_INTERVAL_MS = 50
|
||||||
|
|
||||||
@ -29,21 +31,21 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
self.waypoints: List[Waypoint] = []
|
self.waypoints: List[Waypoint] = []
|
||||||
is_editing = target_to_edit is not None
|
is_editing = target_to_edit is not None
|
||||||
if is_editing:
|
if is_editing:
|
||||||
self.target_id = target_to_edit.target_id
|
# Ensure we are working with a valid Target object
|
||||||
self.waypoints = copy.deepcopy(target_to_edit.trajectory)
|
target = cast(Target, target_to_edit)
|
||||||
self.use_spline_var = tk.BooleanVar(value=bool(getattr(target_to_edit, 'use_spline', False)))
|
self.target_id = target.target_id
|
||||||
|
self.waypoints = copy.deepcopy(target.trajectory)
|
||||||
|
self.use_spline_var = tk.BooleanVar(value=target.use_spline)
|
||||||
else:
|
else:
|
||||||
self.target_id = next((i for i in range(MIN_TARGET_ID, MAX_TARGET_ID + 1) if i not in existing_ids), -1)
|
self.target_id = next((i for i in range(MIN_TARGET_ID, MAX_TARGET_ID + 1) if i not in existing_ids), -1)
|
||||||
self.use_spline_var = tk.BooleanVar(value=False)
|
self.use_spline_var = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
# Accept scenario_name as argument, fallback to default if not provided
|
scenario_name_str = scenario_name or f"Target_{self.target_id}"
|
||||||
if scenario_name is None:
|
self.title(f"Trajectory Editor - {scenario_name_str}")
|
||||||
scenario_name = f"Trajectory_{self.target_id}"
|
|
||||||
self.title(f"Trajectory Editor - {scenario_name} - Target {self.target_id}")
|
|
||||||
|
|
||||||
self.geometry("1100x700")
|
self.geometry("1100x700")
|
||||||
|
|
||||||
self.gui_update_queue = Queue()
|
self.gui_update_queue: Queue = Queue()
|
||||||
self.preview_engine: Optional[SimulationEngine] = None
|
self.preview_engine: Optional[SimulationEngine] = None
|
||||||
self.is_preview_running = tk.BooleanVar(value=False)
|
self.is_preview_running = tk.BooleanVar(value=False)
|
||||||
|
|
||||||
@ -53,34 +55,11 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
self._create_initial_waypoint()
|
self._create_initial_waypoint()
|
||||||
|
|
||||||
self._populate_waypoint_list()
|
self._populate_waypoint_list()
|
||||||
# Aggiorna la preview in base allo stato della checkbox spline
|
|
||||||
self.after(10, self._update_static_preview)
|
self.after(10, self._update_static_preview)
|
||||||
# Se c'è almeno un waypoint, mostra subito la preview del punto iniziale
|
|
||||||
if self.waypoints:
|
|
||||||
self.ppi_preview.clear_previews()
|
|
||||||
if self.use_spline_var.get():
|
|
||||||
temp_target = Target(
|
|
||||||
target_id=self.target_id,
|
|
||||||
trajectory=copy.deepcopy(self.waypoints),
|
|
||||||
use_spline=True
|
|
||||||
)
|
|
||||||
temp_target.reset_simulation()
|
|
||||||
self.ppi_preview.draw_trajectory_preview(temp_target)
|
|
||||||
else:
|
|
||||||
self.ppi_preview.draw_trajectory_preview([self.waypoints[0]])
|
|
||||||
self.ppi_preview.canvas.draw()
|
|
||||||
#self._center_window()
|
|
||||||
|
|
||||||
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
||||||
self.wait_window(self)
|
self.wait_window(self)
|
||||||
|
|
||||||
def _center_window(self):
|
|
||||||
self.update_idletasks()
|
|
||||||
parent = self.master.winfo_toplevel()
|
|
||||||
x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
|
|
||||||
y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
|
|
||||||
self.geometry(f"+{x}+{y}")
|
|
||||||
|
|
||||||
def _create_widgets(self):
|
def _create_widgets(self):
|
||||||
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||||
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
@ -89,100 +68,75 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
main_pane.add(left_frame, weight=1)
|
main_pane.add(left_frame, weight=1)
|
||||||
|
|
||||||
list_frame = ttk.LabelFrame(left_frame, text="Trajectory Waypoints")
|
list_frame = ttk.LabelFrame(left_frame, text="Trajectory Waypoints")
|
||||||
list_frame.pack(fill=tk.BOTH, expand=True)
|
list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
|
||||||
self._create_waypoint_list_widgets(list_frame)
|
self._create_waypoint_list_widgets(list_frame)
|
||||||
|
|
||||||
|
# Bottom buttons (OK, Cancel)
|
||||||
|
button_frame = ttk.Frame(left_frame)
|
||||||
|
button_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(5, 0))
|
||||||
|
ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# Preview Pane
|
||||||
preview_frame = ttk.LabelFrame(main_pane, text="Trajectory Preview")
|
preview_frame = ttk.LabelFrame(main_pane, text="Trajectory Preview")
|
||||||
main_pane.add(preview_frame, weight=2)
|
main_pane.add(preview_frame, weight=2)
|
||||||
|
self._create_preview_widgets(preview_frame)
|
||||||
self.ppi_preview = PPIDisplay(preview_frame, max_range_nm=self.initial_max_range)
|
|
||||||
self.ppi_preview.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
preview_controls = ttk.Frame(preview_frame)
|
|
||||||
preview_controls.pack(fill=tk.X, pady=5)
|
|
||||||
self.play_button = ttk.Button(preview_controls, text="▶ Play", command=self._on_preview_play)
|
|
||||||
self.play_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
self.pause_button = ttk.Button(preview_controls, text="⏸ Pause", command=self._on_preview_pause, state=tk.DISABLED)
|
|
||||||
self.pause_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
self.stop_button = ttk.Button(preview_controls, text="■ Stop", command=self._on_preview_stop, state=tk.DISABLED)
|
|
||||||
self.stop_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
self.reset_button = ttk.Button(preview_controls, text="⟲ Reset", command=self._on_preview_reset)
|
|
||||||
self.reset_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
# ...existing code...
|
|
||||||
ttk.Label(preview_controls, text="Speed:").pack(side=tk.LEFT, padx=(10, 2), pady=5)
|
|
||||||
self.time_multiplier_var = tk.StringVar(value="1x")
|
|
||||||
self.multiplier_combo = ttk.Combobox(preview_controls, textvariable=self.time_multiplier_var, values=["1x", "2x", "4x", "10x", "20x"], state="readonly", width=4)
|
|
||||||
self.multiplier_combo.pack(side=tk.LEFT, padx=(0, 5), pady=5)
|
|
||||||
self.multiplier_combo.bind("<<ComboboxSelected>>", self._on_time_multiplier_changed)
|
|
||||||
|
|
||||||
# --- Progress Bar ---
|
|
||||||
progress_frame = ttk.Frame(preview_frame)
|
|
||||||
progress_frame.pack(fill=tk.X, pady=(5, 0))
|
|
||||||
self.sim_progress_var = tk.DoubleVar(value=0.0)
|
|
||||||
self.sim_progress = ttk.Scale(progress_frame, variable=self.sim_progress_var, from_=0.0, to=1.0, orient=tk.HORIZONTAL, command=self._on_progress_seek)
|
|
||||||
self.sim_progress.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
|
||||||
self.sim_time_label = ttk.Label(progress_frame, text="0.0s / 0.0s")
|
|
||||||
self.sim_time_label.pack(side=tk.RIGHT, padx=5)
|
|
||||||
|
|
||||||
button_frame = ttk.Frame(self)
|
|
||||||
button_frame.pack(fill=tk.X, padx=10, pady=(0, 10), side=tk.BOTTOM)
|
|
||||||
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
|
|
||||||
ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5)
|
|
||||||
def _on_spline_toggle(self):
|
|
||||||
# Conta i punti effettivi (waypoint + calcolati)
|
|
||||||
waypoints = self.waypoints
|
|
||||||
points = []
|
|
||||||
curr_r = None
|
|
||||||
curr_theta = None
|
|
||||||
from target_simulator.core.models import ManeuverType, NM_TO_FT
|
|
||||||
for i, wp in enumerate(waypoints):
|
|
||||||
if getattr(wp, 'maneuver_type', None) == ManeuverType.FLY_TO_POINT:
|
|
||||||
curr_r = getattr(wp, 'target_range_nm', 0)
|
|
||||||
curr_theta = math.radians(getattr(wp, 'target_azimuth_deg', 0))
|
|
||||||
points.append((curr_r, curr_theta))
|
|
||||||
elif getattr(wp, 'maneuver_type', None) == ManeuverType.FLY_FOR_DURATION:
|
|
||||||
if curr_r is None or curr_theta is None:
|
|
||||||
continue
|
|
||||||
vel_fps = getattr(wp, 'target_velocity_fps', 0)
|
|
||||||
vel_nmps = vel_fps / NM_TO_FT if vel_fps else 0
|
|
||||||
duration = getattr(wp, 'duration_s', 0)
|
|
||||||
heading_deg = getattr(wp, 'target_heading_deg', 0)
|
|
||||||
heading_rad = math.radians(heading_deg)
|
|
||||||
dr = vel_nmps * duration
|
|
||||||
theta1 = curr_theta + heading_rad
|
|
||||||
r1 = curr_r + dr
|
|
||||||
points.append((r1, theta1))
|
|
||||||
curr_r = r1
|
|
||||||
curr_theta = theta1
|
|
||||||
if len(points) < 4:
|
|
||||||
self.use_spline_var.set(False)
|
|
||||||
messagebox.showinfo("Spline not available", "Spline mode requires at least 4 points (explicit or calculated waypoints).")
|
|
||||||
return
|
|
||||||
self._update_static_preview()
|
|
||||||
# Aggiorna la label tempo simulazione all'apertura
|
|
||||||
total_time = self._get_total_simulation_time()
|
|
||||||
self.sim_time_label.config(text=f"0.0s / {total_time:.1f}s")
|
|
||||||
|
|
||||||
def _create_waypoint_list_widgets(self, parent):
|
def _create_waypoint_list_widgets(self, parent):
|
||||||
# Checkbox per spline sotto i tasti della tabella
|
|
||||||
self.spline_checkbox = ttk.Checkbutton(parent, text="Use Splined Trajectory", variable=self.use_spline_var, command=self._on_spline_toggle)
|
|
||||||
self.spline_checkbox.pack(fill=tk.X, padx=5, pady=(5, 0))
|
|
||||||
|
|
||||||
self.wp_tree = ttk.Treeview(parent, columns=("num", "type", "details"), show="headings", height=10)
|
self.wp_tree = ttk.Treeview(parent, columns=("num", "type", "details"), show="headings", height=10)
|
||||||
self.wp_tree.heading("num", text="#")
|
self.wp_tree.heading("num", text="#")
|
||||||
self.wp_tree.heading("type", text="Maneuver")
|
self.wp_tree.heading("type", text="Maneuver")
|
||||||
self.wp_tree.heading("details", text="Parameters")
|
self.wp_tree.heading("details", text="Parameters")
|
||||||
self.wp_tree.column("num", width=30, anchor=tk.CENTER)
|
self.wp_tree.column("num", width=30, anchor=tk.CENTER, stretch=False)
|
||||||
self.wp_tree.column("type", width=120)
|
self.wp_tree.column("type", width=120, stretch=False)
|
||||||
self.wp_tree.column("details", width=250)
|
self.wp_tree.column("details", width=250)
|
||||||
self.wp_tree.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
||||||
|
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.wp_tree.yview)
|
||||||
|
self.wp_tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
self.wp_tree.grid(row=0, column=0, sticky="nsew")
|
||||||
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
|
parent.grid_rowconfigure(0, weight=1)
|
||||||
|
parent.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self.wp_tree.bind('<Double-1>', self._on_edit_waypoint)
|
self.wp_tree.bind('<Double-1>', self._on_edit_waypoint)
|
||||||
|
|
||||||
btn_frame = ttk.Frame(parent)
|
btn_frame = ttk.Frame(parent)
|
||||||
btn_frame.pack(fill=tk.X, pady=5)
|
btn_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(5,0))
|
||||||
ttk.Button(btn_frame, text="Add Waypoint", command=self._on_add_waypoint).pack(side=tk.LEFT, padx=2)
|
|
||||||
ttk.Button(btn_frame, text="Edit Waypoint", command=self._on_edit_waypoint).pack(side=tk.LEFT, padx=2)
|
ttk.Button(btn_frame, text="Add", command=self._on_add_waypoint).pack(side=tk.LEFT, padx=(0,2))
|
||||||
ttk.Button(btn_frame, text="Remove Waypoint", command=self._on_remove_waypoint).pack(side=tk.LEFT, padx=2)
|
ttk.Button(btn_frame, text="Edit", command=self._on_edit_waypoint).pack(side=tk.LEFT, padx=2)
|
||||||
|
ttk.Button(btn_frame, text="Remove", command=self._on_remove_waypoint).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
self.spline_checkbox = ttk.Checkbutton(btn_frame, text="Use Spline", variable=self.use_spline_var, command=self._on_spline_toggle)
|
||||||
|
self.spline_checkbox.pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
|
def _create_preview_widgets(self, parent):
|
||||||
|
self.ppi_preview = PPIDisplay(parent, max_range_nm=self.initial_max_range)
|
||||||
|
self.ppi_preview.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
preview_controls = ttk.Frame(parent)
|
||||||
|
preview_controls.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
self.play_button = ttk.Button(preview_controls, text="▶ Play", command=self._on_preview_play)
|
||||||
|
self.play_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.stop_button = ttk.Button(preview_controls, text="■ Stop", command=self._on_preview_stop, state=tk.DISABLED)
|
||||||
|
self.stop_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
ttk.Label(preview_controls, text="Speed:").pack(side=tk.LEFT, padx=(10, 2))
|
||||||
|
self.time_multiplier_var = tk.StringVar(value="1x")
|
||||||
|
self.multiplier_combo = ttk.Combobox(preview_controls, textvariable=self.time_multiplier_var, values=["1x", "2x", "4x", "10x", "20x"], state="readonly", width=4)
|
||||||
|
self.multiplier_combo.pack(side=tk.LEFT)
|
||||||
|
self.multiplier_combo.bind("<<ComboboxSelected>>", self._on_time_multiplier_changed)
|
||||||
|
|
||||||
|
def _on_spline_toggle(self):
|
||||||
|
# A simple spline requires at least 4 waypoints to be meaningful.
|
||||||
|
if self.use_spline_var.get() and len(self.waypoints) < 4:
|
||||||
|
self.use_spline_var.set(False)
|
||||||
|
messagebox.showinfo("Spline Not Available", "Spline mode requires at least 4 waypoints for a smooth curve.", parent=self)
|
||||||
|
return
|
||||||
|
self._update_static_preview()
|
||||||
|
|
||||||
def _create_initial_waypoint(self):
|
def _create_initial_waypoint(self):
|
||||||
"""Forces the user to create the first waypoint (initial position)."""
|
"""Forces the user to create the first waypoint (initial position)."""
|
||||||
@ -194,16 +148,17 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
self.after(10, self._on_cancel)
|
self.after(10, self._on_cancel)
|
||||||
|
|
||||||
def _populate_waypoint_list(self):
|
def _populate_waypoint_list(self):
|
||||||
|
"""Refreshes the treeview with the current list of waypoints."""
|
||||||
self.wp_tree.delete(*self.wp_tree.get_children())
|
self.wp_tree.delete(*self.wp_tree.get_children())
|
||||||
for i, wp in enumerate(self.waypoints):
|
for i, wp in enumerate(self.waypoints):
|
||||||
details = ""
|
details = ""
|
||||||
if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
|
if wp.maneuver_type == ManeuverType.FLY_TO_POINT:
|
||||||
details = f"R:{wp.target_range_nm:.1f}, Az:{wp.target_azimuth_deg:.1f}, Alt:{wp.target_altitude_ft:.0f}, T:{wp.duration_s:.1f}s"
|
details = f"R:{wp.target_range_nm:.1f}, Az:{wp.target_azimuth_deg:.1f}, Alt:{wp.target_altitude_ft:.0f}"
|
||||||
elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
|
elif wp.maneuver_type == ManeuverType.FLY_FOR_DURATION:
|
||||||
vel_kn = (wp.target_velocity_fps or 0.0) * FPS_TO_KNOTS
|
vel_kn = (wp.target_velocity_fps or 0.0) * FPS_TO_KNOTS
|
||||||
details = f"Vel:{vel_kn:.1f}kn, Hdg:{wp.target_heading_deg:.1f}°, T:{wp.duration_s:.1f}s"
|
details = f"Vel:{vel_kn:.1f}kn, Hdg:{wp.target_heading_deg:.1f}°, T:{wp.duration_s:.1f}s"
|
||||||
|
|
||||||
self.wp_tree.insert("", tk.END, iid=str(i), values=(i, wp.maneuver_type.value, details))
|
self.wp_tree.insert("", tk.END, iid=str(i), values=(i + 1, wp.maneuver_type.value, details))
|
||||||
|
|
||||||
def _on_add_waypoint(self):
|
def _on_add_waypoint(self):
|
||||||
editor = WaypointEditorWindow(self, is_first_waypoint=False)
|
editor = WaypointEditorWindow(self, is_first_waypoint=False)
|
||||||
@ -211,28 +166,30 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
self.waypoints.append(editor.result_waypoint)
|
self.waypoints.append(editor.result_waypoint)
|
||||||
self._populate_waypoint_list()
|
self._populate_waypoint_list()
|
||||||
self._update_static_preview()
|
self._update_static_preview()
|
||||||
|
|
||||||
# Select the newly added item
|
# Select the newly added item
|
||||||
new_item_id = str(len(self.waypoints) - 1)
|
new_item_id = str(len(self.waypoints) - 1)
|
||||||
self.wp_tree.focus(new_item_id); self.wp_tree.selection_set(new_item_id)
|
self.wp_tree.see(new_item_id)
|
||||||
|
self.wp_tree.focus(new_item_id)
|
||||||
|
self.wp_tree.selection_set(new_item_id)
|
||||||
|
|
||||||
def _on_edit_waypoint(self, event=None):
|
def _on_edit_waypoint(self, event=None):
|
||||||
selected_item = self.wp_tree.focus()
|
selected_item = self.wp_tree.focus()
|
||||||
if not selected_item:
|
if not selected_item:
|
||||||
if not event:
|
if not event: # Only show warning on button click
|
||||||
messagebox.showwarning("No Selection", "Please select a waypoint to edit.", parent=self)
|
messagebox.showwarning("No Selection", "Please select a waypoint to edit.", parent=self)
|
||||||
return
|
return
|
||||||
|
|
||||||
wp_index = int(selected_item)
|
wp_index = int(selected_item)
|
||||||
waypoint_to_edit = self.waypoints[wp_index]
|
editor = WaypointEditorWindow(self,
|
||||||
is_first = (wp_index == 0)
|
is_first_waypoint=(wp_index == 0),
|
||||||
|
waypoint_to_edit=self.waypoints[wp_index])
|
||||||
# Passa l'indice come parametro
|
|
||||||
editor = WaypointEditorWindow(self, is_first_waypoint=is_first, waypoint_to_edit=waypoint_to_edit, waypoint_index=wp_index)
|
|
||||||
if editor.result_waypoint:
|
if editor.result_waypoint:
|
||||||
self.waypoints[wp_index] = editor.result_waypoint
|
self.waypoints[wp_index] = editor.result_waypoint
|
||||||
self._populate_waypoint_list()
|
self._populate_waypoint_list()
|
||||||
self._update_static_preview()
|
self._update_static_preview()
|
||||||
self.wp_tree.focus(selected_item); self.wp_tree.selection_set(selected_item)
|
self.wp_tree.focus(selected_item)
|
||||||
|
self.wp_tree.selection_set(selected_item)
|
||||||
|
|
||||||
def _on_remove_waypoint(self):
|
def _on_remove_waypoint(self):
|
||||||
selected_item = self.wp_tree.focus()
|
selected_item = self.wp_tree.focus()
|
||||||
@ -240,12 +197,11 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
messagebox.showwarning("No Selection", "Please select a waypoint to remove.", parent=self)
|
messagebox.showwarning("No Selection", "Please select a waypoint to remove.", parent=self)
|
||||||
return
|
return
|
||||||
|
|
||||||
wp_index = int(selected_item)
|
|
||||||
# Prevent deleting the last waypoint, which defines the start position
|
|
||||||
if len(self.waypoints) == 1:
|
if len(self.waypoints) == 1:
|
||||||
messagebox.showerror("Action Denied", "Cannot remove the initial position waypoint. Edit it instead.", parent=self)
|
messagebox.showerror("Action Denied", "Cannot remove the initial position waypoint.", parent=self)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
wp_index = int(selected_item)
|
||||||
del self.waypoints[wp_index]
|
del self.waypoints[wp_index]
|
||||||
self._populate_waypoint_list()
|
self._populate_waypoint_list()
|
||||||
self._update_static_preview()
|
self._update_static_preview()
|
||||||
@ -254,22 +210,11 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
"""Draws the static trajectory path on the PPI."""
|
"""Draws the static trajectory path on the PPI."""
|
||||||
if self.is_preview_running.get():
|
if self.is_preview_running.get():
|
||||||
self._on_preview_stop()
|
self._on_preview_stop()
|
||||||
self.ppi_preview.clear_previews()
|
|
||||||
self.ppi_preview.update_targets([])
|
|
||||||
|
|
||||||
# Passa direttamente la lista o il Target in base alla flag spline
|
self.ppi_preview.draw_trajectory_preview(
|
||||||
if self.use_spline_var.get():
|
waypoints=copy.deepcopy(self.waypoints),
|
||||||
temp_target = Target(
|
use_spline=self.use_spline_var.get()
|
||||||
target_id=self.target_id,
|
)
|
||||||
trajectory=copy.deepcopy(self.waypoints),
|
|
||||||
use_spline=True
|
|
||||||
)
|
|
||||||
temp_target.reset_simulation()
|
|
||||||
self.ppi_preview.draw_trajectory_preview(temp_target)
|
|
||||||
else:
|
|
||||||
self.ppi_preview.draw_trajectory_preview(copy.deepcopy(self.waypoints))
|
|
||||||
# Forza l'aggiornamento della canvas
|
|
||||||
self.ppi_preview.canvas.draw()
|
|
||||||
|
|
||||||
def _on_time_multiplier_changed(self, event=None):
|
def _on_time_multiplier_changed(self, event=None):
|
||||||
"""Handles changes to the time multiplier selection."""
|
"""Handles changes to the time multiplier selection."""
|
||||||
@ -284,17 +229,16 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
def _on_preview_play(self):
|
def _on_preview_play(self):
|
||||||
if self.is_preview_running.get(): return
|
if self.is_preview_running.get(): return
|
||||||
if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
|
if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
|
||||||
messagebox.showinfo("Incomplete Trajectory", "Add a 'Fly to Point' waypoint as the starting position.", parent=self)
|
messagebox.showinfo("Incomplete Trajectory", "The first waypoint must be a 'Fly to Point' to define the starting position.", parent=self)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.is_preview_running.set(True)
|
self.is_preview_running.set(True)
|
||||||
self._update_preview_button_states()
|
self._update_preview_button_states()
|
||||||
|
|
||||||
if self.use_spline_var.get():
|
preview_target = Target(target_id=self.target_id,
|
||||||
preview_target = Target(target_id=self.target_id, trajectory=copy.deepcopy(self.waypoints), use_spline=True)
|
trajectory=copy.deepcopy(self.waypoints),
|
||||||
else:
|
use_spline=self.use_spline_var.get())
|
||||||
preview_target = Target(target_id=self.target_id, trajectory=copy.deepcopy(self.waypoints), use_spline=False)
|
preview_scenario = Scenario(name=f"Preview_{self.target_id}")
|
||||||
preview_scenario = Scenario(name=f"Trajectory_{self.target_id}")
|
|
||||||
preview_scenario.add_target(preview_target)
|
preview_scenario.add_target(preview_target)
|
||||||
|
|
||||||
self.preview_engine = SimulationEngine(communicator=None, update_queue=self.gui_update_queue)
|
self.preview_engine = SimulationEngine(communicator=None, update_queue=self.gui_update_queue)
|
||||||
@ -306,53 +250,45 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
|
|
||||||
def _on_preview_stop(self):
|
def _on_preview_stop(self):
|
||||||
if not self.is_preview_running.get() or not self.preview_engine: return
|
if not self.is_preview_running.get() or not self.preview_engine: return
|
||||||
|
|
||||||
self.preview_engine.stop()
|
self.preview_engine.stop()
|
||||||
self.preview_engine = None
|
self.preview_engine = None
|
||||||
self.is_preview_running.set(False)
|
self.is_preview_running.set(False)
|
||||||
self._update_preview_button_states()
|
self._update_preview_button_states()
|
||||||
self.ppi_preview.update_targets([])
|
|
||||||
|
|
||||||
def _on_preview_reset(self):
|
# After stopping, reset the preview to the static path
|
||||||
if self.is_preview_running.get(): self._on_preview_stop()
|
|
||||||
self._update_static_preview()
|
self._update_static_preview()
|
||||||
|
|
||||||
def _process_preview_queue(self):
|
def _process_preview_queue(self):
|
||||||
try:
|
try:
|
||||||
while not self.gui_update_queue.empty():
|
while not self.gui_update_queue.empty():
|
||||||
updated_targets: List[Target] = self.gui_update_queue.get_nowait()
|
update = self.gui_update_queue.get_nowait()
|
||||||
self.ppi_preview.update_targets(updated_targets)
|
if update == 'SIMULATION_FINISHED':
|
||||||
# Aggiorna la progress bar e la label tempo
|
self._on_preview_stop()
|
||||||
if self.preview_engine and self.preview_engine.scenario:
|
elif isinstance(update, list):
|
||||||
# Prendi il target simulato
|
updated_targets: List[Target] = update
|
||||||
targets = self.preview_engine.scenario.get_all_targets()
|
self.ppi_preview.update_targets(updated_targets)
|
||||||
if targets:
|
|
||||||
t = targets[0]
|
|
||||||
# Usa l'attributo _sim_time_s del target
|
|
||||||
sim_time = getattr(t, '_sim_time_s', 0.0)
|
|
||||||
total_time = self._get_total_simulation_time()
|
|
||||||
progress = min(sim_time / total_time, 1.0) if total_time > 0 else 0.0
|
|
||||||
self.sim_progress_var.set(progress)
|
|
||||||
self.sim_time_label.config(text=f"{sim_time:.1f}s / {total_time:.1f}s")
|
|
||||||
# Se la simulazione è finita (tutti i waypoint raggiunti), ferma la preview e aggiorna i tasti
|
|
||||||
if self.preview_engine and hasattr(self.preview_engine, 'scenario') and self.preview_engine.scenario:
|
|
||||||
if hasattr(self.preview_engine.scenario, 'is_finished') and self.preview_engine.scenario.is_finished:
|
|
||||||
self._on_preview_stop()
|
|
||||||
finally:
|
finally:
|
||||||
if self.is_preview_running.get():
|
if self.is_preview_running.get():
|
||||||
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_preview_queue)
|
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_preview_queue)
|
||||||
|
|
||||||
def _update_preview_button_states(self):
|
def _update_preview_button_states(self):
|
||||||
is_running = self.is_preview_running.get()
|
is_running = self.is_preview_running.get()
|
||||||
state = tk.DISABLED if is_running else tk.NORMAL
|
|
||||||
|
|
||||||
self.play_button.config(state=tk.DISABLED if is_running else tk.NORMAL)
|
self.play_button.config(state=tk.DISABLED if is_running else tk.NORMAL)
|
||||||
self.stop_button.config(state=tk.NORMAL if is_running else tk.DISABLED)
|
self.stop_button.config(state=tk.NORMAL if is_running else tk.DISABLED)
|
||||||
self.multiplier_combo.config(state="readonly" if not is_running else tk.DISABLED)
|
self.multiplier_combo.config(state="readonly" if not is_running else tk.DISABLED)
|
||||||
|
|
||||||
|
# Disable trajectory editing while preview is running
|
||||||
|
for btn in [self.wp_tree.master.children[f] for f in self.wp_tree.master.children if isinstance(self.wp_tree.master.children[f], ttk.Button)]:
|
||||||
|
btn.config(state = tk.DISABLED if is_running else tk.NORMAL)
|
||||||
|
self.spline_checkbox.config(state=tk.DISABLED if is_running else tk.NORMAL)
|
||||||
self.wp_tree.config(selectmode="browse" if not is_running else "none")
|
self.wp_tree.config(selectmode="browse" if not is_running else "none")
|
||||||
|
|
||||||
|
|
||||||
def _on_ok(self):
|
def _on_ok(self):
|
||||||
if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
|
if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
|
||||||
messagebox.showerror("Invalid Trajectory", "The first waypoint must be of type 'Fly to Point' to define a starting position.", parent=self)
|
messagebox.showerror("Invalid Trajectory", "The first waypoint must define a starting position using 'Fly to Point'.", parent=self)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -361,37 +297,12 @@ class TrajectoryEditorWindow(tk.Toplevel):
|
|||||||
trajectory=self.waypoints,
|
trajectory=self.waypoints,
|
||||||
use_spline=self.use_spline_var.get()
|
use_spline=self.use_spline_var.get()
|
||||||
)
|
)
|
||||||
self.destroy() # Chiamiamo destroy() invece di _on_cancel() per evitare di impostare result_target a None
|
self.destroy()
|
||||||
except (ValueError, tk.TclError) as e:
|
except (ValueError, tk.TclError) as e:
|
||||||
messagebox.showerror("Validation Error", str(e), parent=self)
|
messagebox.showerror("Validation Error", str(e), parent=self)
|
||||||
|
|
||||||
def _on_cancel(self):
|
def _on_cancel(self):
|
||||||
if self.is_preview_running.get(): self._on_preview_stop()
|
if self.is_preview_running.get():
|
||||||
|
self._on_preview_stop()
|
||||||
self.result_target = None
|
self.result_target = None
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def _on_preview_pause(self):
|
|
||||||
# Pausa la simulazione preview
|
|
||||||
if self.preview_engine and self.is_preview_running.get():
|
|
||||||
self.preview_engine.pause()
|
|
||||||
self.is_preview_running.set(False)
|
|
||||||
self._update_preview_button_states()
|
|
||||||
|
|
||||||
|
|
||||||
def _on_progress_seek(self, value):
|
|
||||||
# Sposta la simulazione al tempo selezionato
|
|
||||||
total_time = self._get_total_simulation_time()
|
|
||||||
seek_time = float(value) * total_time
|
|
||||||
if self.preview_engine:
|
|
||||||
self.preview_engine.set_simulation_time(seek_time)
|
|
||||||
# Aggiorna la preview grafica
|
|
||||||
updated_targets = self.preview_engine.scenario.get_all_targets() if self.preview_engine.scenario else []
|
|
||||||
self.ppi_preview.update_targets(updated_targets)
|
|
||||||
self.sim_time_label.config(text=f"{seek_time:.1f}s / {total_time:.1f}s")
|
|
||||||
|
|
||||||
def _get_total_simulation_time(self):
|
|
||||||
# Calcola la durata totale della simulazione sommando le durate dei waypoint
|
|
||||||
total = 0.0
|
|
||||||
for wp in self.waypoints:
|
|
||||||
total += wp.duration_s or 0.0
|
|
||||||
return total
|
|
||||||
Loading…
Reference in New Issue
Block a user