refactoring di preview e simulazione usando traiettorie calcolate

This commit is contained in:
VALLONGOL 2025-10-15 10:14:58 +02:00
parent 288c4f73be
commit 1575c1b38a
7 changed files with 569 additions and 867 deletions

View File

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

1 x_nm y_nm
2 0.17452406437283513 1.7364817766693033 9.998476951563912 9.84807753012208
3 0.2859422147817867 1.8196915585801245 10.279638104623308 9.997286852043835
4 0.4421393353133215 1.909698355298528 10.609933583832394 10.164705993254746
5 0.636432547474389 2.006314644389864 10.985187585712282 10.349354914685591
6 0.8621389727719386 2.1093529034194827 11.401224306784082 10.550253577267155
7 1.1125757327129193 2.218625609952734 11.853867943568899 10.766421941930222
8 1.3810599488042814 2.3339452415549684 12.338942692587844 10.99687996960557
9 1.6609087425529734 2.4551242757915364 12.852272750362022 11.240647621223985
10 1.9454392354659444 2.581975190227787 13.389682313412546 11.496744857716246
11 2.227968549050145 2.714310462429072 13.946995578260525 11.764191640013136
12 2.5018138048125236 2.851942569960741 14.520036741427065 12.04200792904544
13 2.7602921242600305 2.9946839903881437 15.104629999433275 12.329213685743936
14 2.996720628899614 3.14234720127663 15.696599548800267 12.624828871039403
15 3.2044164402382243 3.2947446801915516 16.291769586049142 12.927873445862636
16 3.3766966797828104 3.4516889046982575 16.885964307701016 13.237367371144403
17 3.5068784690403216 3.612992352362098 17.475007910277 13.552330607815493
18 3.588278929517707 3.7784675007484245 18.054724590298196 13.871783116806688
19 3.6142151827219164 3.947926827422586 18.620938544285714 14.19474485904877
20 3.578004350159902 4.121182809949932 19.169473968760666 14.520235795472518
21 3.472963553338606 4.298047925895815 19.696155060244156 14.847275887008719
22 3.4729635533386065 4.478334652825583 19.69615506024416 15.17488509458815
23 3.272762644392875 4.661855468304586 20.217383756990923 15.502083379141595
24 2.964843068011936 4.848422849898178 20.750866222507394 15.827890701599843
25 2.5631793345343286 5.037849275171705 21.294382991192247 16.151327022893664
26 2.081745954298589 5.2299472216905185 21.845714597444157 16.471412303953848
27 1.5345174376432555 5.424529167019969 22.402641575661793 16.787166505711173
28 0.9354682949068648 5.621407588725407 22.962944460243826 17.09760958909643
29 0.2985730364279542 5.8203949643721815 23.52440378558893 17.401761515040384
30 -0.36219382745493856 6.021303771525644 24.084800086095772 17.698642244473834
31 -1.032857786403277 6.223946487751144 24.641913896163032 17.987271738327554
32 -1.699444330078522 6.4281355906140325 25.193525750189377 18.26666995753233
33 -2.347978948142139 6.6336835576796584 25.737416182573483 18.535856863018942
34 -2.964487130255587 6.840402866513372 26.271365727714013 18.79385241571817
35 -3.5349943660803307 6.840402866513374 26.793154920009652 18.79385241571817
36 -4.045526145277833 7.064412663195302 27.30056429385906 19.043619595904882
37 -4.482107957509558 7.319986102939524 27.79137438366092 19.28896147814137
38 -4.830765292436963 7.604166677012322 28.263365723813894 19.530158164789555
39 -5.077523639721516 7.913997876679983 28.714318848716662 19.767489758211376
40 -5.208408489024677 8.246523193208787 29.14201429276789 20.001236360768743
41 -5.2094453300079095 8.598786117865023 29.544232590366253 20.231678074823588
42 -5.2094453300079095 8.96783014191497 29.544232590366242 20.459095002737843
43 -5.087215806911271 9.350698756624908 29.93160758132983 20.683767246873423
44 -4.8634013589893685 9.744435453261131 30.31504050771131 20.90597490959226
45 -4.54667716842549 10.146083723089918 30.69271300726126 21.12599809325628
46 -4.145718417402923 10.552687057377549 31.062806717730222 21.3441169002274
47 -3.6692002881049564 10.961288947390312 31.42350327686877 21.560611432867567
48 -3.1257979627148766 11.368932884394491 31.772984322427458 21.775761793538678
49 -2.5241866234159707 11.772662359656369 32.10943149215684 21.989848084602684
50 -1.873041452391528 12.169520864442228 32.43102642380748 22.203150408421493
51 -1.1810376318248348 12.556551890018351 32.735950755129934 22.41594886735704
52 -0.4568503438991798 12.930798927651024 33.02238612387475 22.62852356377125
53 0.29084522920215017 13.289305468606532 33.28851416779252 22.84115460002604
54 1.0533739052958677 13.629115004151155 33.53251652463377 23.054122078483353
55 1.822060502198683 13.947271025551181 33.75257483214907 23.267706101505095
56 2.5882298377273103 14.24081702407289 33.94687072808898 23.482186771453204
57 3.343206729698462 14.506796490982568 34.113585850204075 23.6978441906896
58 4.078315995928851 14.742252917546494 34.25090183624488 23.91495846157622
59 4.784882454235189 14.944229795030958 34.35700032396197 24.133809686474972
60 5.454230922434187 15.109770614702246 34.430062951105924 24.354677967747794
61 6.077686218342558 15.23591886782663 34.46827135542727 24.57784340775661
62 6.077686218342562 15.319718045670406 34.468271355427284 24.80358610886334
63 6.6950615437258465 15.358211639499848 34.47971290071672 25.032186173429917
64 7.3476044195818755 15.348443140581244 34.472832854810775 25.26392370381826
65 8.028791142575807 15.28745604018088 34.44634971152879 25.499078802390304
66 8.732098009372796 15.172293829565035 34.3989819646901 25.737931571507964
67 9.451001316637997 15.0 34.32944810811404 25.980762113533167
68 10.178977361036566 14.999999999999998 34.23646663561996 25.98076211353316
69 10.909502439233654 14.76007873005941 34.11875604102719 26.230939963143115
70 11.636052847894423 14.447076186168827 33.97503481815507 26.491050501357915
71 12.352104883684024 14.065598841372388 33.80402146082294 26.760197600053854
72 13.051134843267612 13.62025316871424 33.60443446285013 27.037485131107207
73 13.726619023310347 13.11564564123853 33.37499231805599 27.322016966394273
74 14.372033720477376 12.556382731989396 33.114413520259845 27.612896977791323
75 14.98085523143386 11.947070914010986 32.82141656328105 27.909229037174644
76 15.546559852844958 11.292316660347446 32.49471994093893 28.210117016420522
77 16.062623881375814 10.596726444042913 32.13304214705283 28.514664787405238
78 16.522523613691593 9.86490673814154 31.735101675442085 28.82197622200508
79 16.919735346457447 9.101464015687464 31.29961701992603 29.13115519209633
80 17.247735376338525 8.311004749724834 30.825306674324008 29.441305569555276
81 17.499999999999996 7.498135413297791 30.310889132455358 29.751531226258194
82 17.499999999999996 6.66746247945048 30.310889132455355 30.060936034081376
83 17.667951456825268 5.823592421227048 29.722378176057024 30.368623864901103
84 17.752649951768674 4.971131711671633 29.034858901953363 30.67369859059366
85 17.76202872985173 4.114686823828384 28.26065677659477 30.97526408303533
86 17.704021036095945 3.2588642307414455 27.412097266431687 31.272424214102394
87 17.586560115522854 2.4082704054549575 26.501505837914515 31.564282855671145
88 17.417579213153953 1.5675118210130687 25.541207957493693 31.84994387961786
89 17.205011574010783 0.7411949504599216 24.54352909161962 32.12851115781883
90 16.956790443114837 -0.06607373316034426 23.52079470674273 32.39908856215034
91 16.68084906548765 -0.8496877568035757 22.485330269313444 32.660779964488654
92 16.385120686150735 -1.6050406474256338 21.449461245782175 32.91268923671007
93 16.0775385501256 -2.3275259319823753 20.425513102599343 33.15392025069089
94 15.766035902433776 -3.0125371374296517 19.42581130621538 33.38357687830738
95 15.45854598809677 -3.6554677907233284 18.462681323080695 33.600762991435815
96 15.1630020521361 -4.2517114188192515 17.5484486196457 33.80458246195249
97 14.88733733957329 -4.796661548673278 16.69543866236083 33.99413916173369
98 14.639485095429851 -5.285711707241269 15.9159769176765 34.1685369626557
99 14.427378564727302 -5.714255421479081 15.222388852043133 34.326879736594805
100 14.258950992487165 -6.077686218342556 14.626999931911136 34.46827135542728
14.142135623730947 14.142135623730947

View File

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

View File

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

View File

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

View File

@ -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.")

View File

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

View File

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