remove README.Protocol and add a README that refers to the "real"
[fg:toms-fgdata.git] / Nasal / aircraft.nas
1 # These classes provide basic functions for use in aircraft specific
2 # Nasal context. Note that even if a class is called "door" or "light"
3 # this doesn't mean that it can't be used for other purposes.
4 #
5 # Class instances don't have to be assigned to variables. They do also
6 # work if they remain anonymous. It's even a good idea to keep them
7 # anonymous if you don't need further access to their members. On the
8 # other hand, you can assign the class and apply setters at the same time:
9 #
10 #   aircraft.light.new("sim/model/foo/beacon", [1, 1]);    # anonymous
11 #   var strobe = aircraft.light.new("sim/model/foo/strobe", [1, 1]).cont().switch(1);
12 #
13 #
14 # Classes do create properties, but they don't usually overwrite the contents
15 # of an existing property. This makes it possible to preset them in
16 # a *-set.xml file or on the command line. For example:
17 #
18 #   $ fgfs --aircraft=bo105 --prop:/controls/doors/door[0]/position-norm=1
19 #
20 #
21 # Wherever a property argument can be given, this can either be a path,
22 # or a node (i.e. property node hash). In return, the property node can
23 # always be accessed directly as member "node", and turned into a path
24 # string with node.getPath():
25 #
26 #   var beacon = aircraft.light.new("sim/model/foo/beacon", [1, 1]);
27 #   print(beacon.node.getPath());
28 #
29 #   var strobe_node = props.globals.getNode("sim/model/foo/strobe", 1);
30 #   var strobe = aircraft.light.new(strobe_node, [0.05, 1.0]);
31 #
32 #
33 # The classes implement only commonly used features, but are easy to
34 # extend, as all class members are accessible from outside. For example:
35 #
36 #   # add custom property to door node:
37 #   frontdoor.node.getNode("name", 1).setValue("front door");
38 #
39 #   # add method to class instance (or base class -> aircraft.door.print)
40 #   frontdoor.print = func { print(me.position.getValue()) };
41 #
42 #
43
44 # constants
45 # ==============================================================================
46 var D2R = math.pi / 180;
47 var R2D = 180 / math.pi;
48
49
50
51
52 # helper functions
53 # ==============================================================================
54
55 # creates (if necessary) and returns a property node from arg[0],
56 # which can be a property node already, or a property path
57 #
58 var makeNode = func(n) {
59         if (isa(n, props.Node))
60                 return n;
61         else
62                 return props.globals.getNode(n, 1);
63 }
64
65
66 # returns arg[1]-th optional argument of vector arg[0] or default value arg[2]
67 #
68 var optarg = func {
69         if (size(arg[0]) > arg[1] and arg[0][arg[1]] != nil)
70                 arg[0][arg[1]];
71         else
72                 arg[2];
73 }
74
75
76
77
78 # door
79 # ==============================================================================
80 # class for objects moving at constant speed, with the ability to
81 # reverse moving direction at any point. Appropriate for doors, canopies, etc.
82 #
83 # SYNOPSIS:
84 #       door.new(<property>, <swingtime> [, <startpos>]);
85 #
86 #       property   ... door node: property path or node
87 #       swingtime  ... time in seconds for full movement (0 -> 1)
88 #       startpos   ... initial position      (default: 0)
89 #
90 # PROPERTIES:
91 #       ./position-norm   (double)     (default: <startpos>)
92 #       ./enabled         (bool)       (default: 1)
93 #
94 # EXAMPLE:
95 #       var canopy = aircraft.door.new("sim/model/foo/canopy", 5);
96 #       canopy.open();
97 #
98 var door = {
99         new : func {
100                 m = { parents : [door] };
101                 m.node = makeNode(arg[0]);
102                 m.swingtime = arg[1];
103                 m.positionN = m.node.getNode("position-norm", 1);
104                 m.enabledN = m.node.getNode("enabled", 1);
105                 if (m.enabledN.getValue() == nil)
106                         m.enabledN.setBoolValue(1);
107
108                 pos = optarg(arg, 2, 0);
109                 if (m.positionN.getValue() == nil)
110                         m.positionN.setDoubleValue(pos);
111
112                 m.target = pos < 0.5;
113                 return m;
114         },
115         # door.enable(bool)    ->  set ./enabled
116         enable  : func { me.enabledN.setBoolValue(arg[0]); me },
117
118         # door.setpos(double)  ->  set ./position-norm without movement
119         setpos  : func { me.positionN.setValue(arg[0]); me.target = arg[0] < 0.5; me },
120
121         # double door.getpos() ->  return current position as double
122         getpos  : func { me.positionN.getValue() },
123
124         # door.close()         ->  move to closed state
125         close   : func { me.move(me.target = 0) },
126
127         # door.open()          ->  move to open state
128         open    : func { me.move(me.target = 1) },
129
130         # door.toggle()        ->  move to opposite end position
131         toggle  : func { me.move(me.target) },
132
133         # door.stop()          ->  stop movement
134         stop    : func { interpolate(me.positionN) },
135
136         # door.move(double)    ->  move to arbitrary position
137         move    : func {
138                 time = abs(me.getpos() - arg[0]) * me.swingtime;
139                 interpolate(me.positionN, arg[0], time);
140                 me.target = !me.target;
141         },
142 };
143
144
145
146 # light
147 # ==============================================================================
148 # class for generation of pulsing values. Appropriate for controlling
149 # beacons, strobes, etc.
150 #
151 # SYNOPSIS:
152 #       light.new(<property>, <pattern> [, <switch>]);
153 #       light.new(<property>, <stretch>, <pattern> [, <switch>]);
154 #
155 #       property   ... light node: property path or node
156 #       stretch    ... multiplicator for all pattern values
157 #       pattern    ... array of on/off time intervals (in seconds)
158 #       switch     ... property path or node to use as switch   (default: ./enabled)
159 #                      instead of ./enabled
160 #
161 # PROPERTIES:
162 #       ./state           (bool)   (default: 0)
163 #       ./enabled         (bool)   (default: 0) except if <switch> given)
164 #
165 # EXAMPLES:
166 #       aircraft.light.new("sim/model/foo/beacon", [0.4, 0.4]);    # anonymous light
167 #-------
168 #       var strobe = aircraft.light.new("sim/model/foo/strobe", [0.05, 0.05, 0.05, 1],
169 #                       "controls/lighting/strobe");
170 #       strobe.switch(1);
171 #-------
172 #       var switch = props.globals.getNode("controls/lighting/strobe", 1);
173 #       var pattern = [0.02, 0.03, 0.02, 1];
174 #       aircraft.light.new("sim/model/foo/strobe-top", 1.001, pattern, switch);
175 #       aircraft.light.new("sim/model/foo/strobe-bot", 1.005, pattern, switch);
176 #
177 var light = {
178         new : func {
179                 m = { parents : [light] };
180                 m.node = makeNode(arg[0]);
181                 var stretch = 1.0;
182                 var c = 1;
183                 if (typeof(arg[c]) == "scalar") {
184                         stretch = arg[c];
185                         c += 1;
186                 }
187                 if (typeof(arg[c]) != "vector") {
188                         die("aircraft.nas: the arguments of aircraft.light.new() have changed!\n" ~
189                                         "  *** BEFORE: aircraft.light.new(property, 0.1, 0.9, switch)\n" ~
190                                         "  ***    NOW: aircraft.light.new(property, [0.1, 0.9], switch)");
191                 }
192                 m.pattern = arg[c];
193                 c += 1;
194                 if (size(arg) > c and arg[c] != nil)
195                         m.switchN = makeNode(arg[c]);
196                 else
197                         m.switchN = m.node.getNode("enabled", 1);
198
199                 if (m.switchN.getValue() == nil)
200                         m.switchN.setBoolValue(0);
201
202                 m.stateN = m.node.getNode("state", 1);
203                 if (m.stateN.getValue() == nil)
204                         m.stateN.setBoolValue(0);
205
206                 forindex (var i; m.pattern)
207                         m.pattern[i] *= stretch;
208
209                 m.index = 0;
210                 m.loopid = 0;
211                 m.continuous = 0;
212                 m.lastswitch = 0;
213                 m.seqcount = -1;
214                 m.endstate = 0;
215                 m.count = nil;
216                 m.switchL = setlistener(m.switchN, func { m._switch_() }, 1);
217                 return m;
218         },
219         # class destructor
220         del : func {
221                 removelistener(me.switchL);
222         },
223         # light.switch(bool)   ->  set light switch (also affects other lights
224         #                          that use the same switch)
225         switch : func(v) { me.switchN.setBoolValue(v); me },
226
227         # light.toggle()       ->  toggle light switch
228         toggle : func { me.switchN.setBoolValue(!me.switchN.getValue()); me },
229
230         # light.cont()         ->  continuous light
231         cont : func {
232                 if (!me.continuous) {
233                         me.continuous = 1;
234                         me.loopid += 1;
235                         me.stateN.setBoolValue(me.lastswitch);
236                 }
237                 me;
238         },
239
240         # light.blink()        ->  blinking light  (default)
241         # light.blink(3)       ->  when switched on, only run three blink sequences;
242         #                          second optional arg defines state after the sequences
243         blink : func(count = -1, endstate = 0) {
244                 me.seqcount = count;
245                 me.endstate = endstate;
246                 if (me.continuous) {
247                         me.continuous = 0;
248                         me.index = 0;
249                         me.stateN.setBoolValue(0);
250                         me.lastswitch and me._loop_(me.loopid += 1);
251                 }
252                 me;
253         },
254
255         _switch_ : func {
256                 var switch = me.switchN.getBoolValue();
257                 switch != me.lastswitch or return;
258                 me.lastswitch = switch;
259                 me.loopid += 1;
260                 if (me.continuous or !switch) {
261                         me.stateN.setBoolValue(switch);
262                 } elsif (switch) {
263                         me.stateN.setBoolValue(0);
264                         me.index = 0;
265                         me.count = me.seqcount;
266                         me._loop_(me.loopid);
267                 }
268         },
269
270         _loop_ : func(id) {
271                 id == me.loopid or return;
272                 if (!me.count) {
273                         me.loopid += 1;
274                         me.stateN.setBoolValue(me.endstate);
275                         return;
276                 }
277                 me.stateN.setBoolValue(me.index == 2 * int(me.index / 2));
278                 settimer(func { me._loop_(id) }, me.pattern[me.index]);
279                 if ((me.index += 1) >= size(me.pattern)) {
280                         me.index = 0;
281                         if (me.count > 0)
282                                 me.count -= 1;
283                 }
284         },
285 };
286
287
288 # lowpass
289 # ==============================================================================
290 # class that implements a variable-interval EWMA (Exponentially Weighted
291 # Moving Average) lowpass filter with characteristics independent of the
292 # frame rate.
293 #
294 # SYNOPSIS:
295 #       lowpass.new(<coefficient>);
296 #
297 # EXAMPLE:
298 #       var lp = aircraft.lowpass.new(0.5);
299 #       print(lp.filter(10));  # prints 10
300 #       print(lp.filter(0));
301 #
302 var lowpass = {
303         new : func(coeff) {
304                 var m = { parents : [lowpass] };
305                 m.coeff = coeff >= 0 ? coeff : die("aircraft.lowpass(): coefficient must be >= 0");
306                 m.value = nil;
307                 return m;
308         },
309         # filter(raw_value)    -> push new value, returns filtered value
310         filter : func(v) {
311                 me.filter = me._filter_;
312                 me.value = v;
313         },
314         # get()                -> returns filtered value
315         get : func {
316                 me.value;
317         },
318         # set()                -> sets new average and returns it
319         set : func(v) {
320                 me.value = v;
321         },
322         _filter_ : func(v) {
323                 var dt = getprop("/sim/time/delta-realtime-sec");
324                 var c = dt / (me.coeff + dt);
325                 me.value = v * c + me.value * (1 - c);
326         },
327 };
328
329
330 # angular lowpass
331 # ==============================================================================
332 # same as above, but for angles. Filters sin/cos separately and calculates the
333 # angle again from them. This avoids unexpected jumps from 180 to -179.99 degree.
334 #
335 var angular_lowpass = {
336         new : func(coeff) {
337                 var m = { parents : [angular_lowpass] };
338                 m.sin = lowpass.new(coeff);
339                 m.cos = lowpass.new(coeff);
340                 return m;
341         },
342         filter : func(v) {
343                 v *= D2R;
344                 math.atan2(me.sin.filter(math.sin(v)), me.cos.filter(math.cos(v))) * R2D;
345         },
346         set : func(v) {
347                 v *= D2R;
348                 me.sin.set(math.sin(v));
349                 me.cos.set(math.cos(v));
350         },
351         get : func {
352                 math.atan2(me.sin.get(), me.cos.get()) * R2D;
353         },
354 };
355
356
357 # data
358 # ==============================================================================
359 # class that loads and saves properties to aircraft-specific data files in
360 # ~/.fgfs/aircraft-data/ (Unix) or %APPDATA%\flightgear.org\aircraft-data\.
361 # There's no public constructor, as the only needed instance gets created
362 # by the system.
363 #
364 # SYNOPSIS:
365 #       data.add(<properties>);
366 #       data.save([<interval>])
367 #
368 #       properties  ... about any combination of property nodes (props.Node)
369 #                       or path name strings, or lists or hashes of them,
370 #                       lists of lists of them, etc.
371 #       interval    ... save in <interval> minutes intervals, or only once
372 #                       if 'nil' or empty (and again at reinit/exit)
373 #
374 # SIGNALS:
375 #       /sim/signals/save   ... set to 'true' right before saving. Can be used
376 #                               to update values that are to be saved
377 #
378 # EXAMPLE:
379 #       var p = props.globals.getNode("/sim/model", 1);
380 #       var vec = [p, p];
381 #       var hash = {"foo": p, "bar": p};
382 #
383 #       # add properties
384 #       aircraft.data.add("/sim/fg-root", p, "/sim/fg-home");
385 #       aircraft.data.add(p, vec, hash, "/sim/fg-root");
386 #
387 #       # now save only once (and at exit/reinit, which is automatically done)
388 #       aircraft.data.save();
389 #
390 #       # or save now and every 30 sec (and at exit/reinit)
391 #       aircraft.data.save(0.5);
392 #
393 var data = {
394         init : func {
395                 me.path = getprop("/sim/fg-home") ~ "/aircraft-data/" ~ getprop("/sim/aircraft") ~ ".xml";
396                 me.signalN = props.globals.getNode("/sim/signals/save", 1);
397                 me.catalog = [];
398                 me.loopid = 0;
399                 me.interval = 0;
400
401                 setlistener("/sim/signals/reinit", func { cmdarg().getBoolValue() and me._save_() });
402                 setlistener("/sim/signals/exit", func { me._save_() });
403         },
404         load : func {
405                 printlog("warn", "trying to load aircraft data from ", me.path, " (OK if not found)");
406                 fgcommand("load", props.Node.new({ "file": me.path }));
407         },
408         save : func(v = nil) {
409                 me.loopid += 1;
410                 if (v == nil) {
411                         me._save_();
412                 } else {
413                         me.interval = 60 * v;
414                         me._loop_(me.loopid);
415                 }
416         },
417         _loop_ : func(id) {
418                 id == me.loopid or return;
419                 me._save_();
420                 settimer(func { me._loop_(id) }, me.interval);
421         },
422         _save_ : func {
423                 size(me.catalog) or return;
424                 printlog("info", "saving aircraft data to ", me.path);
425                 me.signalN.setBoolValue(1);
426                 var args = props.Node.new({ "filename": me.path });
427                 var data = args.getNode("data", 1);
428                 foreach (var c; me.catalog) {
429                         if (c[0] == `/`)
430                                 c = substr(c, 1);
431
432                         props.copy(props.globals.getNode(c, 1), data.getNode(c, 1));
433                 }
434                 fgcommand("savexml", args);
435         },
436         add : func {
437                 foreach (var a; arg) {
438                         var t = typeof(a);
439                         if (isa(a, props.Node)) {
440                                 append(me.catalog, a.getPath());
441                         } elsif (t == "scalar") {
442                                 append(me.catalog, a);
443                         } elsif (t == "vector") {
444                                 foreach (var i; a)
445                                         me.add(i);
446                         } elsif (t == "hash") {
447                                 foreach (var i; keys(a))
448                                         me.add(a[i]);
449                         } else {
450                                 die("aircraft.data.add(): invalid item of type " ~ t);
451                         }
452                 }
453         },
454 };
455
456
457 # timer
458 # ==============================================================================
459 # class that implements timer that can be started, stopped, reset, and can
460 # have its value saved to the aircraft specific data file. Saving the value
461 # is done automatically by the aircraft.Data class.
462 #
463 # SYNOPSIS:
464 #       timer.new(<property> [, <resolution:double> [, <save:bool>]])
465 #
466 #       <property>   ... property path or props.Node hash that holds the timer value
467 #       <resolution> ... timer update resolution -- interval in seconds in which the
468 #                        timer property is updated while running (default: 1 s)
469 #       <save>       ... bool that defines whether the timer value should be saved
470 #                        and restored next time, as needed for Hobbs meters
471 #                        (default: 1)
472 #
473 # EXAMPLES:
474 #       var hobbs_turbine = aircraft.timer.new("/sim/time/hobbs/turbine[0]", 60);
475 #       hobbs_turbine.start();
476 #       
477 #       aircraft.timer.new("/sim/time/hobbs/battery", 60).start();  # anonymous timer
478 #
479 var timer = {
480         new : func(prop, res = 1, save = 1) {
481                 var m = { parents : [timer] };
482                 m.node = makeNode(prop);
483                 if (m.node.getType() == "NONE")
484                         m.node.setDoubleValue(0);
485
486                 m.systimeN = props.globals.getNode("/sim/time/elapsed-sec", 1);
487                 m.last_systime = nil;
488                 m.interval = res;
489                 m.loopid = 0;
490                 m.running = 0;
491                 if (save) {
492                         data.add(m.node);
493                         m.saveL = setlistener("/sim/signals/save", func { m._save_() });
494                 } else {
495                         m.saveL = nil;
496                 }
497                 return m;
498         },
499         del : func {
500                 me.stop();
501                 if (me.saveL != nil)
502                         removelistener(me.saveL);
503         },
504         start : func {
505                 me.running and return;
506                 me.last_systime = me.systimeN.getValue();
507                 me.interval != nil and me._loop_(me.loopid);
508                 me.running = 1;
509                 me;
510         },
511         stop : func {
512                 me.running or return;
513                 me.running = 0;
514                 me.loopid += 1;
515                 me._apply_();
516         },
517         reset : func {
518                 me.node.setDoubleValue(0);
519                 me.last_systime = me.systimeN.getValue();
520         },
521         _apply_ : func {
522                 var sys = me.systimeN.getValue();
523                 me.node.setDoubleValue(me.node.getValue() + sys - me.last_systime);
524                 me.last_systime = sys;
525         },
526         _save_ : func {
527                 if (me.running)
528                         me._apply_();
529         },
530         _loop_ : func(id) {
531                 id != me.loopid and return;
532                 me._apply_();
533                 settimer(func { me._loop_(id) }, me.interval);
534         },
535 };
536
537
538
539 # livery
540 # =============================================================================
541 # Class that maintains livery XML files (see English Electric Lightning for an
542 # example). The last used livery is saved on exit and restored next time. Livery
543 # files are regular PropertyList XML files whose properties are copied to the
544 # main tree (whereby the node types are ignored).
545 #
546 # SYNOPSIS:
547 #       livery.init(<livery-dir> [, <name-path> [, <sort-path>]]);
548 #
549 #       <livery-dir> ... directory with livery XML files, relative to $FG_ROOT
550 #       <name-path>  ... property path to the livery name in the livery files
551 #                        and the property tree (default: /sim/model/livery/name)
552 #       <sort-path>  ... property path to the sort criterion (default: same as
553 #                        <name-path> -- that is: alphabetic sorting)
554 #
555 # EXAMPLE:
556 #       aircraft.livery.init("Aircraft/Lightning/Models/Liveries",
557 #                            "sim/model/livery/variant",
558 #                            "sim/model/livery/index");  # optional
559 #
560 #       aircraft.livery.dialog.toggle();
561 #       aircraft.livery.select("OEBH");
562 #       aircraft.livery.next();
563 #
564 var livery = {
565         init : func(livery_dir, name_path = "sim/model/livery/name", sort_path = nil) {
566                 me.dir = livery_dir;
567                 if (me.dir[-1] != `/`)
568                         me.dir ~= "/";
569                 me.name_path = name_path;
570                 me.sort_path = sort_path != nil ? sort_path : name_path;
571                 me.rescan();
572                 aircraft.data.add(name_path);
573                 me.dialog = gui.Dialog.new("livery-select");
574         },
575         rescan : func {
576                 me.data = [];
577                 var path = getprop("/sim/fg-root") ~ "/" ~ me.dir;
578                 foreach (var file; directory(path)) {
579                         if (substr(file, -4) != ".xml")
580                                 continue;
581                         var n = props.Node.new({ filename : path ~ file });
582                         fgcommand("loadxml", n);
583                         n = n.getNode("data");
584
585                         var name = n.getNode(me.name_path);
586                         var index = n.getNode(me.sort_path);
587                         if (name == nil or index == nil)
588                                 continue;
589
590                         append(me.data, [name.getValue(), index.getValue(), n.getValues()]);
591                 }
592                 me.data = sort(me.data, func(a, b) {
593                         num(a[1]) == nil or num(b[1]) == nil ? cmp(a[1], b[1]) : a[1] - b[1];
594                 });
595                 me.select(getprop(me.name_path));
596         },
597         # select by index (out-of-bounds indices are wrapped)
598         set : func(i) {
599                 if (i < 0)
600                         i = size(me.data - 1);
601                 if (i >= size(me.data))
602                         i = 0;
603                 props.globals.setValues(me.data[i][2]);
604                 me.current = i;
605         },
606         # select by name
607         select : func(name) {
608                 forindex (var i; me.data)
609                         if (me.data[i][0] == name)
610                                 me.set(i);
611         },
612         next : func {
613                 me.set(me.current + 1);
614         },
615         previous : func {
616                 me.set(me.current - 1);
617         },
618 };
619
620
621
622 # steering
623 # =============================================================================
624 # Class that implements differential braking depending on rudder position.
625 # Note that this overrides the controls.applyBrakes() wrapper. If you need
626 # your own version, then override it again after the steering.init() call.
627 #
628 # SYNOPSIS:
629 #       steering.init([<property> [, <threshold>]]);
630 #
631 #       <property>  ... property path or props.Node hash that enables/disables
632 #                       brake steering (usually bound to the js trigger button)
633 #       <threshold> ... defines range (+- threshold) around neutral rudder
634 #                       position in which both brakes are applied
635 #
636 # EXAMPLES:
637 #       aircraft.steering.init("/controls/gear/steering", 0.2);
638 #       aircraft.steering.init();
639 #
640 var steering = {
641         init : func(switch = "/controls/gear/brake-steering", threshold = 0.3) {
642                 me.threshold = threshold;
643                 me.switchN = makeNode(switch);
644                 me.switchN.setBoolValue(me.switchN.getBoolValue());
645                 me.leftN = props.globals.getNode("/controls/gear/brake-left", 1);
646                 me.rightN = props.globals.getNode("/controls/gear/brake-right", 1);
647                 me.rudderN = props.globals.getNode("/controls/flight/rudder", 1);
648                 me.loopid = 0;
649
650                 controls.applyBrakes = func(v, w = 0) {
651                         call(func(v, w) (w < 0 ? leftN : w > 0 ? rightN : switchN).setValue(v),
652                                         [v, w], nil, aircraft.steering);
653                 }
654                 setlistener(me.switchN, func {
655                         me.loopid += 1;
656                         if (cmdarg().getValue())
657                                 me._loop_(me.loopid);
658                         else
659                                 me.setbrakes(0, 0);
660                 }, 1);
661         },
662         _loop_ : func(id) {
663                 id == me.loopid or return;
664                 var rudder = me.rudderN.getValue();
665                 if (rudder > me.threshold)
666                         me.setbrakes(0, rudder);
667                 elsif (rudder < -me.threshold)
668                         me.setbrakes(-rudder, 0);
669                 else
670                         me.setbrakes(1, 1);
671
672                 settimer(func { me._loop_(id) }, 0);
673         },
674         setbrakes : func(left, right) {
675                 me.leftN.setDoubleValue(left);
676                 me.rightN.setDoubleValue(right);
677         },
678 };
679
680
681
682 # autotrim
683 # =============================================================================
684 # Singleton class that supports quick trimming and compensates for the lack
685 # of resistance/force feedback in most joysticks. Normally the pilot trims such
686 # that no real or artificially generated (by means of servo motors and spring
687 # preloading) forces act on the stick/yoke and it is in a comfortable position.
688 # This doesn't work well on computer joysticks.
689 #
690 # SYNOPSIS:
691 #       autotrim.start();  # on key/button press
692 #       autotrim.stop();   # on key/button release (mod-up)
693 #
694 # USAGE:
695 #       (1) move the stick such that the aircraft is in an orientation that
696 #           you want to trim for (forward flight, hover, ...)
697 #       (2) press autotrim button and keep it pressed
698 #       (3) move stick/yoke to neutral position (center)
699 #       (4) release autotrim button
700 #
701 var autotrim = {
702         init : func {
703                 me.elevator = me.Trim.new("elevator");
704                 me.aileron = me.Trim.new("aileron");
705                 me.rudder = me.Trim.new("rudder");
706                 me.loopid = 0;
707                 me.active = 0;
708         },
709         start : func {
710                 me.active and return;
711                 me.active = 1;
712                 me.elevator.start();
713                 me.aileron.start();
714                 me.rudder.start();
715                 me._loop_(me.loopid += 1);
716         },
717         stop : func {
718                 me.active or return;
719                 me.active = 0;
720                 me.loopid += 1;
721                 me.update();
722         },
723         _loop_ : func(id) {
724                 id == me.loopid or return;
725                 me.update();
726                 settimer(func { me._loop_(id) }, 0);
727         },
728         update : func {
729                 me.elevator.update();
730                 me.aileron.update();
731                 me.rudder.update();
732         },
733         Trim : {
734                 new : func(name) {
735                         var m = { parents : [ autotrim.Trim ] };
736                         m.trimN = props.globals.getNode("/controls/flight/" ~ name ~ "-trim", 1);
737                         m.ctrlN = props.globals.getNode("/controls/flight/" ~ name, 1);
738                         return m;
739                 },
740                 start : func {
741                         me.last = me.ctrlN.getValue();
742                 },
743                 update : func {
744                         var v = me.ctrlN.getValue();
745                         me.trimN.setDoubleValue(me.trimN.getValue() + me.last - v);
746                         me.last = v;
747                 },
748         },
749 };
750
751
752
753 # HUD control class to handle both HUD implementations
754 # ==============================================================================
755 #
756 var HUD = {
757         init : func {
758                 me.vis0N = props.globals.getNode("/sim/hud/visibility[0]", 1);
759                 me.vis1N = props.globals.getNode("/sim/hud/visibility[1]", 1);
760                 me.currcolN = props.globals.getNode("/sim/hud/current-color", 1);
761                 me.paletteN = props.globals.getNode("/sim/hud/palette", 1);
762                 me.brightnessN = props.globals.getNode("/sim/hud/color/brightness", 1);
763                 me.currentN = me.vis0N;
764         },
765         cycle_color : func {            # h-key
766                 if (!me.currentN.getBoolValue())                # if off, turn on
767                         return me.currentN.setBoolValue(1);
768
769                 var i = me.currcolN.getValue() + 1;             # if through, turn off
770                 if (i < 0 or i >= size(me.paletteN.getChildren("color"))) {
771                         me.currentN.setBoolValue(0);
772                         me.currcolN.setIntValue(0);
773                 } else {                                        # otherwise change color
774                         me.currentN.setBoolValue(1);
775                         me.currcolN.setIntValue(i);
776                 }
777         },
778         cycle_brightness : func {       # H-key
779                 me.is_active() or return;
780                 var br = me.brightnessN.getValue() - 0.2;
781                 me.brightnessN.setValue(br > 0.01 ? br : 1);
782         },
783         normal_type : func {            # i-key
784                 me.is_active() or return;
785                 me.oldinit1();
786                 me.vis0N.setBoolValue(1);
787                 me.vis1N.setBoolValue(0);
788                 me.currentN = me.vis0N;
789         },
790         cycle_type : func {             # I-key
791                 me.is_active() or return;
792                 if (me.currentN == me.vis0N) {
793                         me.vis0N.setBoolValue(0);
794                         me.vis1N.setBoolValue(1);
795                         me.currentN = me.vis1N;
796                 } elsif (me.currentN == me.vis1N) {
797                         me.vis0N.setBoolValue(1);
798                         me.vis1N.setBoolValue(0);
799                         me.oldinit2();
800                         me.currentN = me.vis0N;
801                 }
802         },
803         oldinit1 : func { fgcommand("hud-init") },
804         oldinit2 : func { fgcommand("hud-init2") },
805         is_active : func { me.vis0N.getValue() or me.vis1N.getValue() },
806 };
807
808
809
810 # module initialization
811 # ==============================================================================
812 #
813
814 _setlistener("/sim/signals/nasal-dir-initialized", func {
815
816         props.globals.getNode("/sim/time/delta-realtime-sec", 1).setDoubleValue(0.00000001);
817         HUD.init();
818         data.init();
819         autotrim.init();
820
821         if (getprop("/sim/startup/save-on-exit")) {
822                 data.load();
823                 var n = props.globals.getNode("/sim/aircraft-data");
824                 if (n != nil)
825                         foreach (var c; n.getChildren("path"))
826                                 if (c.getType() != "NONE")
827                                         data.add(c.getValue());
828         } else {
829                 data._save_ = func {}
830                 data._loop_ = func {}
831         }
832 });
833
834