Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / aircraft.nas
1 # This module provide basic functions and classes for use in aircraft specific
2 # Nasal context.
3
4
5
6 # helper functions
7 # ==============================================================================
8
9 # creates (if necessary) and returns a property node from arg[0],
10 # which can be a property node already, or a property path
11 #
12 var makeNode = func(n) {
13         if (isa(n, props.Node))
14                 return n;
15         else
16                 return props.globals.getNode(n, 1);
17 }
18
19
20 # returns args[index] if available and non-nil, or default otherwise
21 #
22 var optarg = func(args, index, default) {
23         size(args) > index and args[index] != nil ? args[index] : default;
24 }
25
26
27
28 # door
29 # ==============================================================================
30 # class for objects moving at constant speed, with the ability to
31 # reverse moving direction at any point. Appropriate for doors, canopies, etc.
32 #
33 # SYNOPSIS:
34 #       door.new(<property>, <swingtime> [, <startpos>]);
35 #
36 #       property   ... door node: property path or node
37 #       swingtime  ... time in seconds for full movement (0 -> 1)
38 #       startpos   ... initial position      (default: 0)
39 #
40 # PROPERTIES:
41 #       ./position-norm   (double)     (default: <startpos>)
42 #       ./enabled         (bool)       (default: 1)
43 #
44 # EXAMPLE:
45 #       var canopy = aircraft.door.new("sim/model/foo/canopy", 5);
46 #       canopy.open();
47 #
48 var door = {
49         new: func(node, swingtime, pos = 0) {
50                 var m = { parents: [door] };
51                 m.node = makeNode(node);
52                 m.swingtime = swingtime;
53                 m.enabledN = m.node.initNode("enabled", 1, "BOOL");
54                 m.positionN = m.node.initNode("position-norm", pos);
55                 m.target = pos < 0.5;
56                 return m;
57         },
58         # door.enable(bool)    ->  set ./enabled
59         enable: func(v) {
60                 me.enabledN.setBoolValue(v);
61                 me;
62         },
63         # door.setpos(double)  ->  set ./position-norm without movement
64         setpos: func(pos) {
65                 me.stop();
66                 me.positionN.setValue(pos);
67                 me.target = pos < 0.5;
68                 me;
69         },
70         # double door.getpos() ->  return current position as double
71         getpos: func {
72                 me.positionN.getValue();
73         },
74         # door.close()         ->  move to closed state
75         close: func {
76                 me.move(me.target = 0);
77         },
78         # door.open()          ->  move to open state
79         open: func {
80                 me.move(me.target = 1);
81         },
82         # door.toggle()        ->  move to opposite end position
83         toggle: func {
84                 me.move(me.target);
85         },
86         # door.stop()          ->  stop movement
87         stop: func {
88                 interpolate(me.positionN);
89         },
90         # door.move(double)    ->  move to arbitrary position
91         move: func(target) {
92                 var pos = me.getpos();
93                 if (pos != target) {
94                         var time = abs(pos - target) * me.swingtime;
95                         interpolate(me.positionN, target, time);
96                 }
97                 me.target = !me.target;
98         },
99 };
100
101
102
103 # light
104 # ==============================================================================
105 # class for generation of pulsing values. Appropriate for controlling
106 # beacons, strobes, etc.
107 #
108 # SYNOPSIS:
109 #       light.new(<property>, <pattern> [, <switch>]);
110 #       light.new(<property>, <stretch>, <pattern> [, <switch>]);
111 #
112 #       property   ... light node: property path or node
113 #       stretch    ... multiplicator for all pattern values
114 #       pattern    ... array of on/off time intervals (in seconds)
115 #       switch     ... property path or node to use as switch   (default: ./enabled)
116 #                      instead of ./enabled
117 #
118 # PROPERTIES:
119 #       ./state           (bool)   (default: 0)
120 #       ./enabled         (bool)   (default: 0) except if <switch> given)
121 #
122 # EXAMPLES:
123 #       aircraft.light.new("sim/model/foo/beacon", [0.4, 0.4]);    # anonymous light
124 #-------
125 #       var strobe = aircraft.light.new("sim/model/foo/strobe", [0.05, 0.05, 0.05, 1],
126 #                       "controls/lighting/strobe");
127 #       strobe.switch(1);
128 #-------
129 #       var switch = props.globals.getNode("controls/lighting/strobe", 1);
130 #       var pattern = [0.02, 0.03, 0.02, 1];
131 #       aircraft.light.new("sim/model/foo/strobe-top", 1.001, pattern, switch);
132 #       aircraft.light.new("sim/model/foo/strobe-bot", 1.005, pattern, switch);
133 #
134 var light = {
135         new: func {
136                 var m = { parents: [light] };
137                 m.node = makeNode(arg[0]);
138                 var stretch = 1.0;
139                 var c = 1;
140                 if (typeof(arg[c]) == "scalar") {
141                         stretch = arg[c];
142                         c += 1;
143                 }
144                 m.pattern = arg[c];
145                 c += 1;
146                 if (size(arg) > c and arg[c] != nil)
147                         m.switchN = makeNode(arg[c]);
148                 else
149                         m.switchN = m.node.getNode("enabled", 1);
150
151                 m.switchN.initNode(nil, 0, "BOOL");
152                 m.stateN = m.node.initNode("state", 0, "BOOL");
153
154                 forindex (var i; m.pattern)
155                         m.pattern[i] *= stretch;
156
157                 m.index = 0;
158                 m.loopid = 0;
159                 m.continuous = 0;
160                 m.lastswitch = 0;
161                 m.seqcount = -1;
162                 m.endstate = 0;
163                 m.count = nil;
164                 m.switchL = setlistener(m.switchN, func m._switch_(), 1);
165                 return m;
166         },
167         # class destructor
168         del: func {
169                 removelistener(me.switchL);
170         },
171         # light.switch(bool)   ->  set light switch (also affects other lights
172         #                          that use the same switch)
173         switch: func(v) {
174                 me.switchN.setBoolValue(v);
175                 me;
176         },
177         # light.toggle()       ->  toggle light switch
178         toggle: func {
179                 me.switchN.setBoolValue(!me.switchN.getValue());
180                 me;
181         },
182         # light.cont()         ->  continuous light
183         cont: func {
184                 if (!me.continuous) {
185                         me.continuous = 1;
186                         me.loopid += 1;
187                         me.stateN.setBoolValue(me.lastswitch);
188                 }
189                 me;
190         },
191         # light.blink()        ->  blinking light  (default)
192         # light.blink(3)       ->  when switched on, only run three blink sequences;
193         #                          second optional arg defines state after the sequences
194         blink: func(count = -1, endstate = 0) {
195                 me.seqcount = count;
196                 me.endstate = endstate;
197                 if (me.continuous) {
198                         me.continuous = 0;
199                         me.index = 0;
200                         me.stateN.setBoolValue(0);
201                         me.lastswitch and me._loop_(me.loopid += 1);
202                 }
203                 me;
204         },
205         _switch_: func {
206                 var switch = me.switchN.getBoolValue();
207                 switch != me.lastswitch or return;
208                 me.lastswitch = switch;
209                 me.loopid += 1;
210                 if (me.continuous or !switch) {
211                         me.stateN.setBoolValue(switch);
212                 } elsif (switch) {
213                         me.stateN.setBoolValue(0);
214                         me.index = 0;
215                         me.count = me.seqcount;
216                         me._loop_(me.loopid);
217                 }
218         },
219         _loop_: func(id) {
220                 id == me.loopid or return;
221                 if (!me.count) {
222                         me.loopid += 1;
223                         me.stateN.setBoolValue(me.endstate);
224                         return;
225                 }
226                 me.stateN.setBoolValue(me.index == 2 * int(me.index / 2));
227                 settimer(func me._loop_(id), me.pattern[me.index]);
228                 if ((me.index += 1) >= size(me.pattern)) {
229                         me.index = 0;
230                         if (me.count > 0)
231                                 me.count -= 1;
232                 }
233         },
234 };
235
236
237
238 # lowpass
239 # ==============================================================================
240 # class that implements a variable-interval EWMA (Exponentially Weighted
241 # Moving Average) lowpass filter with characteristics independent of the
242 # frame rate.
243 #
244 # SYNOPSIS:
245 #       lowpass.new(<coefficient>);
246 #
247 # EXAMPLE:
248 #       var lp = aircraft.lowpass.new(1.5);
249 #       print(lp.filter(10));  # prints 10
250 #       print(lp.filter(0));
251 #
252 var lowpass = {
253         new: func(coeff) {
254                 var m = { parents: [lowpass] };
255                 m.coeff = coeff >= 0 ? coeff : die("aircraft.lowpass(): coefficient must be >= 0");
256                 m.value = nil;
257                 return m;
258         },
259         # filter(raw_value)    -> push new value, returns filtered value
260         filter: func(v) {
261                 me.filter = me._filter_;
262                 me.value = v;
263         },
264         # get()                -> returns filtered value
265         get: func {
266                 me.value;
267         },
268         # set()                -> sets new average and returns it
269         set: func(v) {
270                 me.value = v;
271         },
272         _filter_: func(v) {
273                 var dt = getprop("/sim/time/delta-sec")*getprop("/sim/speed-up");
274                 var c = dt / (me.coeff + dt);
275                 me.value = v * c + me.value * (1 - c);
276         },
277 };
278
279
280
281 # angular lowpass
282 # ==============================================================================
283 # same as above, but for angles. Filters sin/cos separately and calculates the
284 # angle again from them. This avoids unexpected jumps from 179.99 to -180 degree.
285 #
286 var angular_lowpass = {
287         new: func(coeff) {
288                 var m = { parents: [angular_lowpass] };
289                 m.sin = lowpass.new(coeff);
290                 m.cos = lowpass.new(coeff);
291                 m.value = nil;
292                 return m;
293         },
294         filter: func(v) {
295                 v *= D2R;
296                 me.value = math.atan2(me.sin.filter(math.sin(v)), me.cos.filter(math.cos(v))) * R2D;
297         },
298         set: func(v) {
299                 v *= D2R;
300                 me.sin.set(math.sin(v));
301                 me.cos.set(math.cos(v));
302         },
303         get: func {
304                 me.value;
305         },
306 };
307
308
309
310 # data
311 # ==============================================================================
312 # class that loads and saves properties to aircraft-specific data files in
313 # ~/.fgfs/aircraft-data/ (Unix) or %APPDATA%\flightgear.org\aircraft-data\.
314 # There's no public constructor, as the only needed instance gets created
315 # by the system.
316 #
317 # SYNOPSIS:
318 #       data.add(<properties>);
319 #       data.save([<interval>])
320 #
321 #       properties  ... about any combination of property nodes (props.Node)
322 #                       or path name strings, or lists or hashes of them,
323 #                       lists of lists of them, etc.
324 #       interval    ... save in <interval> minutes intervals, or only once
325 #                       if 'nil' or empty (and again at reinit/exit)
326 #
327 # SIGNALS:
328 #       /sim/signals/save   ... set to 'true' right before saving. Can be used
329 #                               to update values that are to be saved
330 #
331 # EXAMPLE:
332 #       var p = props.globals.getNode("/sim/model", 1);
333 #       var vec = [p, p];
334 #       var hash = {"foo": p, "bar": p};
335 #
336 #       # add properties
337 #       aircraft.data.add("/sim/fg-root", p, "/sim/fg-home");
338 #       aircraft.data.add(p, vec, hash, "/sim/fg-root");
339 #
340 #       # now save only once (and at exit/reinit, which is automatically done)
341 #       aircraft.data.save();
342 #
343 #       # or save now and every 30 sec (and at exit/reinit)
344 #       aircraft.data.save(0.5);
345 #
346 var data = {
347         init: func {
348                 me.path = getprop("/sim/fg-home") ~ "/aircraft-data/" ~ getprop("/sim/aircraft") ~ ".xml";
349                 me.signalN = props.globals.getNode("/sim/signals/save", 1);
350                 me.catalog = [];
351                 me.loopid = 0;
352                 me.interval = 0;
353
354                 setlistener("/sim/signals/reinit", func(n) { n.getBoolValue() and me._save_() });
355                 setlistener("/sim/signals/exit", func me._save_());
356         },
357         load: func {
358                 if (io.stat(me.path) != nil) {
359                         printlog("info", "loading aircraft data from ", me.path);
360                         io.read_properties(me.path, props.globals);
361                 }
362         },
363         save: func(v = nil) {
364                 me.loopid += 1;
365                 if (v == nil) {
366                         me._save_();
367                 } else {
368                         me.interval = 60 * v;
369                         me._loop_(me.loopid);
370                 }
371         },
372         _loop_: func(id) {
373                 id == me.loopid or return;
374                 me._save_();
375                 settimer(func me._loop_(id), me.interval);
376         },
377         _save_: func {
378                 size(me.catalog) or return;
379                 printlog("debug", "saving aircraft data to ", me.path);
380                 me.signalN.setBoolValue(1);
381                 var data = props.Node.new();
382                 foreach (var c; me.catalog) {
383                         if (c[0] == `/`)
384                                 c = substr(c, 1);
385
386                         props.copy(props.globals.getNode(c, 1), data.getNode(c, 1));
387                 }
388                 io.write_properties(me.path, data);
389         },
390         add: func(p...) {
391                 foreach (var n; props.nodeList(p))
392                         append(me.catalog, n.getPath());
393         },
394 };
395
396
397
398 # timer
399 # ==============================================================================
400 # class that implements timer that can be started, stopped, reset, and can
401 # have its value saved to the aircraft specific data file. Saving the value
402 # is done automatically by the aircraft.Data class.
403 #
404 # SYNOPSIS:
405 #       timer.new(<property> [, <resolution:double> [, <save:bool>]])
406 #
407 #       <property>   ... property path or props.Node hash that holds the timer value
408 #       <resolution> ... timer update resolution -- interval in seconds in which the
409 #                        timer property is updated while running (default: 1 s)
410 #       <save>       ... bool that defines whether the timer value should be saved
411 #                        and restored next time, as needed for Hobbs meters
412 #                        (default: 1)
413 #
414 # EXAMPLES:
415 #       var hobbs_turbine = aircraft.timer.new("/sim/time/hobbs/turbine[0]", 60);
416 #       hobbs_turbine.start();
417 #       
418 #       aircraft.timer.new("/sim/time/hobbs/battery", 60).start();  # anonymous timer
419 #
420 var timer = {
421         new: func(prop, res = 1, save = 1) {
422                 var m = { parents: [timer] };
423                 m.node = makeNode(prop);
424                 if (m.node.getType() == "NONE")
425                         m.node.setDoubleValue(0);
426
427                 me.systimeN = props.globals.getNode("/sim/time/elapsed-sec", 1);
428                 m.last_systime = nil;
429                 m.interval = res;
430                 m.loopid = 0;
431                 m.running = 0;
432                 m.reinitL = setlistener("/sim/signals/reinit", func(n) {
433                         if (n.getValue()) {
434                                 m.stop();
435                                 m.total = m.node.getValue();
436                         } else {
437                                 m.node.setDoubleValue(m.total);
438                         }
439                 });
440                 if (save) {
441                         data.add(m.node);
442                         m.saveL = setlistener("/sim/signals/save", func m._save_());
443                 } else {
444                         m.saveL = nil;
445                 }
446                 return m;
447         },
448         del: func {
449                 me.stop();
450                 removelistener(me.reinitL);
451                 if (me.saveL != nil)
452                         removelistener(me.saveL);
453         },
454         start: func {
455                 me.running and return;
456                 me.last_systime = me.systimeN.getValue();
457                 if (me.interval != nil)
458                         me._loop_(me.loopid);
459                 me.running = 1;
460                 me;
461         },
462         stop: func {
463                 me.running or return;
464                 me.running = 0;
465                 me.loopid += 1;
466                 me._apply_();
467                 me;
468         },
469         reset: func {
470                 me.node.setDoubleValue(0);
471                 me.last_systime = me.systimeN.getValue();
472         },
473         _apply_: func {
474                 var sys = me.systimeN.getValue();
475                 me.node.setDoubleValue(me.node.getValue() + sys - me.last_systime);
476                 me.last_systime = sys;
477         },
478         _save_: func {
479                 if (me.running)
480                         me._apply_();
481         },
482         _loop_: func(id) {
483                 id != me.loopid and return;
484                 me._apply_();
485                 settimer(func me._loop_(id), me.interval);
486         },
487 };
488
489
490
491 # livery
492 # =============================================================================
493 # Class that maintains livery XML files (see English Electric Lightning for an
494 # example). The last used livery is saved on exit and restored next time. Livery
495 # files are regular PropertyList XML files whose properties are copied to the
496 # main tree.
497 #
498 # SYNOPSIS:
499 #       livery.init(<livery-dir> [, <name-path> [, <sort-path>]]);
500 #
501 #       <livery-dir> ... directory with livery XML files, relative to $FG_ROOT
502 #       <name-path>  ... property path to the livery name in the livery files
503 #                        and the property tree (default: sim/model/livery/name)
504 #       <sort-path>  ... property path to the sort criterion (default: same as
505 #                        <name-path> -- that is: alphabetic sorting)
506 #
507 # EXAMPLE:
508 #       aircraft.livery.init("Aircraft/Lightning/Models/Liveries",
509 #                            "sim/model/livery/variant",
510 #                            "sim/model/livery/index");  # optional
511 #
512 #       aircraft.livery.dialog.toggle();
513 #       aircraft.livery.select("OEBH");
514 #       aircraft.livery.next();
515 #
516 var livery = {
517         init: func(dir, nameprop = "sim/model/livery/name", sortprop = nil) {
518                 me.parents = [gui.OverlaySelector.new("Select Livery", dir, nameprop,
519                                 sortprop, "sim/model/livery/file")];
520                 me.dialog = me.parents[0];
521         },
522 };
523
524
525
526 # livery_update
527 # =============================================================================
528 # Class for maintaining liveries in MP aircraft. It is used in Nasal code that's
529 # embedded in aircraft animation XML files, and checks in intervals whether the
530 # parent aircraft has changed livery, in which case it changes the livery
531 # in the remote aircraft accordingly. This class is a wrapper for overlay_update.
532 #
533 # SYNOPSIS:
534 #       livery_update.new(<livery-dir> [, <interval:10> [, <func>]]);
535 #
536 #       <livery-dir> ... directory with livery files, relative to $FG_ROOT
537 #       <interval>   ... checking interval in seconds (default: 10)
538 #       <func>       ... callback function that's called with the ./sim/model/livery/file
539 #                        contents as argument whenever the livery has changed. This can
540 #                        be used for post-processing.
541 #
542 # EXAMPLE:
543 #       <nasal>
544 #               <load>
545 #                       var livery_update = aircraft.livery_update.new(
546 #                                       "Aircraft/R22/Models/Liveries", 30,
547 #                                       func print("R22 livery update"));
548 #               </load>
549 #
550 #               <unload>
551 #                       livery_update.stop();
552 #               </unload>
553 #       </nasal>
554 #
555 var livery_update = {
556         new: func(liveriesdir, interval = 10.01, callback = nil) {
557                 var m = { parents: [livery_update, overlay_update.new()] };
558                 m.parents[1].add(liveriesdir, "sim/model/livery/file", callback);
559                 m.parents[1].interval = interval;
560                 return m;
561         },
562         stop: func {
563                 me.parents[1].stop();
564         },
565 };
566
567
568
569 # overlay_update
570 # =============================================================================
571 # Class for maintaining overlays in MP aircraft. It is used in Nasal code that's
572 # embedded in aircraft animation XML files, and checks in intervals whether the
573 # parent aircraft has changed an overlay, in which case it copies the respective
574 # overlay to the aircraft's root directory.
575 #
576 # SYNOPSIS:
577 #       livery_update.new();
578 #       livery_update.add(<overlay-dir>, <property> [, <callback>]);
579 #
580 #       <overlay-dir> ... directory with overlay files, relative to $FG_ROOT
581 #       <property>    ... MP property where the overlay file name can be found
582 #                         (usually one of the sim/multiplay/generic/string properties)
583 #       <callback>    ... callback function that's called with two arguments:
584 #                         the file name (without extension) and the overlay directory
585 #
586 # EXAMPLE:
587 #       <nasal>
588 #               <load>
589 #                       var update = aircraft.overlay_update.new();
590 #                       update.add("Aircraft/F4U/Models/Logos", "sim/multiplay/generic/string");
591 #               </load>
592 #
593 #               <unload>
594 #                       update.stop();
595 #               </unload>
596 #       </nasal>
597 #
598 var overlay_update = {
599         new: func {
600                 var m = { parents: [overlay_update] };
601                 m.root = cmdarg();
602                 m.data = {};
603                 m.interval = 10.01;
604                 if (m.root.getName() == "multiplayer")
605                         m._loop_();
606                 return m;
607         },
608         add: func(path, prop, callback = nil) {
609                 var path = path ~ '/';
610                 me.data[path] = [me.root.initNode(prop, ""), "",
611                                 typeof(callback) == "func" ? callback : func nil];
612                 return me;
613         },
614         stop: func {
615                 me._loop_ = func nil;
616         },
617         _loop_: func {
618                 foreach (var path; keys(me.data)) {
619                         var v = me.data[path];
620                         var file = v[0].getValue();
621                         if (file != v[1]) {
622                                 io.read_properties(path ~ file ~ ".xml", me.root);
623                                 v[2](v[1] = file, path);
624                         }
625                 }
626                 settimer(func me._loop_(), me.interval);
627         },
628 };
629
630
631
632 # steering
633 # =============================================================================
634 # Class that implements differential braking depending on rudder position.
635 # Note that this overrides the controls.applyBrakes() wrapper. If you need
636 # your own version, then override it again after the steering.init() call.
637 #
638 # SYNOPSIS:
639 #       steering.init([<property> [, <threshold>]]);
640 #
641 #       <property>  ... property path or props.Node hash that enables/disables
642 #                       brake steering (usually bound to the js trigger button)
643 #       <threshold> ... defines range (+- threshold) around neutral rudder
644 #                       position in which both brakes are applied
645 #
646 # EXAMPLES:
647 #       aircraft.steering.init("/controls/gear/steering", 0.2);
648 #       aircraft.steering.init();
649 #
650 var steering = {
651         init: func(switch = "/controls/gear/brake-steering", threshold = 0.3) {
652                 me.threshold = threshold;
653                 me.switchN = makeNode(switch);
654                 me.switchN.setBoolValue(me.switchN.getBoolValue());
655                 me.leftN = props.globals.getNode("/controls/gear/brake-left", 1);
656                 me.rightN = props.globals.getNode("/controls/gear/brake-right", 1);
657                 me.rudderN = props.globals.getNode("/controls/flight/rudder", 1);
658                 me.loopid = 0;
659
660                 controls.applyBrakes = func(v, w = 0) {
661                         if (w < 0)
662                                 steering.leftN.setValue(v);
663                         elsif (w > 0)
664                                 steering.rightN.setValue(v);
665                         else
666                                 steering.switchN.setValue(v);
667                 }
668                 setlistener(me.switchN, func(n) {
669                         me.loopid += 1;
670                         if (n.getValue())
671                                 me._loop_(me.loopid);
672                         else
673                                 me.setbrakes(0, 0);
674                 }, 1);
675         },
676         _loop_: func(id) {
677                 id == me.loopid or return;
678                 var rudder = me.rudderN.getValue();
679                 if (rudder > me.threshold)
680                         me.setbrakes(0, rudder);
681                 elsif (rudder < -me.threshold)
682                         me.setbrakes(-rudder, 0);
683                 else
684                         me.setbrakes(1, 1);
685
686                 settimer(func me._loop_(id), 0);
687         },
688         setbrakes: func(left, right) {
689                 me.leftN.setDoubleValue(left);
690                 me.rightN.setDoubleValue(right);
691         },
692 };
693
694
695
696 # autotrim
697 # =============================================================================
698 # Singleton class that supports quick trimming and compensates for the lack
699 # of resistance/force feedback in most joysticks. Normally the pilot trims such
700 # that no real or artificially generated (by means of servo motors and spring
701 # preloading) forces act on the stick/yoke and it is in a comfortable position.
702 # This doesn't work well on computer joysticks.
703 #
704 # SYNOPSIS:
705 #       autotrim.start();  # on key/button press
706 #       autotrim.stop();   # on key/button release (mod-up)
707 #
708 # USAGE:
709 #       (1) move the stick such that the aircraft is in an orientation that
710 #           you want to trim for (forward flight, hover, ...)
711 #       (2) press autotrim button and keep it pressed
712 #       (3) move stick/yoke to neutral position (center)
713 #       (4) release autotrim button
714 #
715 var autotrim = {
716         init: func {
717                 me.elevator = me.Trim.new("elevator");
718                 me.aileron = me.Trim.new("aileron");
719                 me.rudder = me.Trim.new("rudder");
720                 me.loopid = 0;
721                 me.active = 0;
722         },
723         start: func {
724                 me.active and return;
725                 me.active = 1;
726                 me.elevator.start();
727                 me.aileron.start();
728                 me.rudder.start();
729                 me._loop_(me.loopid += 1);
730         },
731         stop: func {
732                 me.active or return;
733                 me.active = 0;
734                 me.loopid += 1;
735                 me.update();
736         },
737         _loop_: func(id) {
738                 id == me.loopid or return;
739                 me.update();
740                 settimer(func me._loop_(id), 0);
741         },
742         update: func {
743                 me.elevator.update();
744                 me.aileron.update();
745                 me.rudder.update();
746         },
747         Trim: {
748                 new: func(name) {
749                         var m = { parents: [autotrim.Trim] };
750                         m.trimN = props.globals.getNode("/controls/flight/" ~ name ~ "-trim", 1);
751                         m.ctrlN = props.globals.getNode("/controls/flight/" ~ name, 1);
752                         return m;
753                 },
754                 start: func {
755                         me.last = me.ctrlN.getValue();
756                 },
757                 update: func {
758                         var v = me.ctrlN.getValue();
759                         me.trimN.setDoubleValue(me.trimN.getValue() + me.last - v);
760                         me.last = v;
761                 },
762         },
763 };
764
765
766
767 # tyresmoke
768 # =============================================================================
769 # Provides a property which can be used to contol particles used to simulate tyre
770 # smoke on landing. Weight on wheels, vertical speed, ground speed, ground friction
771 # factor are taken into account. Tyre slip is simulated by low pass filters.
772 #
773 # Modifications to the model file are required.
774 #
775 # Generic XML particle files are available, but are not mandatory
776 # (see Hawker Seahawk for an example).
777 #
778 # SYNOPSIS:
779 #       aircraft.tyresmoke.new(gear index [, auto = 0])
780 #               gear index - the index of the gear to which the tyre smoke is attached
781 #               auto - enable automatic update (recommended). defaults to 0 for backward compatibility.
782 #       aircraft.tyresmoke.del()
783 #               destructor.
784 #       aircraft.tyresmoke.update()
785 #               Runs the update. Not required if automatic updates are enabled.
786 #
787 # EXAMPLE:
788 #       var tyresmoke_0 = aircraft.tyresmoke.new(0);
789 #       tyresmoke_0.update();
790 #
791 # PARAMETERS:
792 #
793 #    number: index of gear to be animated, i.e. "2" for /gear/gear[2]/...
794 #
795 #    auto: 1 when tyresmoke should start on update loop. 0 when you're going
796 #      to call the update method from one of your own loops.
797 #
798 #    diff_norm: value adjusting the necessary percental change of roll-speed
799 #      to trigger tyre smoke. Default value is 0.05. More realistic results can
800 #      be achieved with significantly higher values (i.e. use 0.8).
801 #
802 #    check_vspeed: 1 when tyre smoke should only be triggered when vspeed is negative
803 #      (usually doesn't work for all gear, since vspeed=0.0 after the first gear touches
804 #      ground). Use 0 to make tyre smoke independent of vspeed.
805 #      Note: in reality, tyre smoke doesn't depend on vspeed, but only on acceleration
806 #      and friction.
807 #
808
809 var tyresmoke = {
810         new: func(number, auto = 0, diff_norm = 0.05, check_vspeed=1) {
811                 var m = { parents: [tyresmoke] };
812                 m.vertical_speed = (!check_vspeed) ? nil : props.globals.initNode("velocities/vertical-speed-fps");
813                 m.diff_norm = diff_norm;
814                 m.speed = props.globals.initNode("velocities/groundspeed-kt");
815                 m.rain = props.globals.initNode("environment/metar/rain-norm");
816
817                 var gear = props.globals.getNode("gear/gear[" ~ number ~ "]/");
818                 m.wow = gear.initNode("wow");
819                 m.tyresmoke = gear.initNode("tyre-smoke", 0, "BOOL");
820                 m.friction_factor = gear.initNode("ground-friction-factor", 1);
821                 m.sprayspeed = gear.initNode("sprayspeed-ms");
822                 m.spray = gear.initNode("spray", 0, "BOOL");
823                 m.spraydensity = gear.initNode("spray-density", 0, "DOUBLE");
824                 m.auto = auto;
825                 m.listener = nil;
826
827                 if (getprop("sim/flight-model") == "jsb") {
828                         var wheel_speed = "fdm/jsbsim/gear/unit[" ~ number ~ "]/wheel-speed-fps";
829                         m.rollspeed = props.globals.initNode(wheel_speed);
830                         m.get_rollspeed = func m.rollspeed.getValue() * 0.3043;
831                 } else {
832                         m.rollspeed = gear.initNode("rollspeed-ms");
833                         m.get_rollspeed = func m.rollspeed.getValue();
834                 }
835
836                 m.lp = lowpass.new(2);
837                 auto and m.update();
838                 return m;
839         },
840         del: func {
841                 if (me.listener != nil) {
842                         removelistener(me.listener);
843                         me.listener = nil;
844                 }
845                 me.auto = 0;
846         },
847         update: func {
848                 var rollspeed = me.get_rollspeed();
849                 var vert_speed = (me.vertical_speed) != nil ? me.vertical_speed.getValue() : -999;
850                 var groundspeed = me.speed.getValue();
851                 var friction_factor = me.friction_factor.getValue();
852                 var wow = me.wow.getValue();
853                 var rain = me.rain.getValue();
854
855                 var filtered_rollspeed = me.lp.filter(rollspeed);
856                 var diff = math.abs(rollspeed - filtered_rollspeed);
857                 var diff_norm = diff > 0 ? diff / rollspeed : 0;
858
859                 if (wow and vert_speed < -1.2
860                                 and diff_norm > me.diff_norm
861                                 and friction_factor > 0.7 and groundspeed > 50
862                                 and rain < 0.20) {
863                         me.tyresmoke.setValue(1);
864                         me.spray.setValue(0);
865                         me.spraydensity.setValue(0);
866                 } elsif (wow and groundspeed > 5 and rain >= 0.20) {
867                         me.tyresmoke.setValue(0);
868                         me.spray.setValue(1);
869                         me.sprayspeed.setValue(rollspeed * 6);
870                         me.spraydensity.setValue(rain * groundspeed);
871                 } else {
872                         me.tyresmoke.setValue(0);
873                         me.spray.setValue(0);
874                         me.sprayspeed.setValue(0);
875                         me.spraydensity.setValue(0);
876                 }
877                 if (me.auto) {
878                         if (wow) {
879                                 settimer(func me.update(), 0);
880                                 if (me.listener != nil) {
881                                         removelistener(me.listener);
882                                         me.listener = nil;
883                                 }
884                         } elsif (me.listener == nil) {
885                                 me.listener = setlistener(me.wow, func me._wowchanged_(), 0, 0);
886                         }
887                 }
888         },
889         _wowchanged_: func() {
890                 if (me.wow.getValue()) {
891                         me.lp.set(0);
892                         me.update();
893                 }
894         },
895 };
896
897 # tyresmoke_system
898 # =============================================================================
899 # Helper class to contain the tyresmoke objects for all the gears.
900 # Will update automatically, nothing else needs to be done by the caller.
901 #
902 # SYNOPSIS:
903 #       aircraft.tyresmoke_system.new(<gear index 1>, <gear index 2>, ...)
904 #               <gear index> - the index of the gear to which the tyre smoke is attached
905 #       aircraft.tyresmoke_system.del()
906 #               destructor
907 # EXAMPLE:
908 #       var tyresmoke_system = aircraft.tyresmoke_system.new(0, 1, 2, 3, 4);
909
910 var tyresmoke_system = {
911         new: func {
912                 var m = { parents: [tyresmoke_system] };
913                 # preset array to proper size
914                 m.gears = [];
915                 setsize(m.gears, size(arg));
916                 for(var i = size(arg) - 1; i >= 0; i -= 1) {
917                         m.gears[i] = tyresmoke.new(arg[i], 1);
918                 }
919                 return m;
920         },
921         del: func {
922                 foreach(var gear; me.gears) {
923                         gear.del();
924                 }
925         }
926 };
927
928 # rain
929 # =============================================================================
930 # Provides a property which can be used to control rain. Can be used to turn
931 # off rain in internal views, and or used with a texture on canopies etc.
932 # The output is co-ordinated with system precipitation:
933 #
934 #       /sim/model/rain/raining-norm  rain intensity
935 #       /sim/model/rain/flow-mps      drop flow speed [m/s]
936 #
937 # See Hawker Seahawk for an example.
938 #
939 # SYNOPSIS:
940 #       aircraft.rain.init();
941 #       aircraft.rain.update();
942 #
943 var rain = {
944         init: func {
945                 me.elapsed_timeN = props.globals.getNode("sim/time/elapsed-sec");
946                 me.dtN = props.globals.getNode("sim/time/delta-sec");
947
948                 me.enableN = props.globals.initNode("sim/rendering/precipitation-aircraft-enable", 0, "BOOL");
949                 me.precip_levelN = props.globals.initNode("environment/params/precipitation-level-ft", 0);
950                 me.altitudeN = props.globals.initNode("position/altitude-ft", 0);
951                 me.iasN = props.globals.initNode("velocities/airspeed-kt", 0);
952                 me.rainingN = props.globals.initNode("sim/model/rain/raining-norm", 0);
953                 me.flowN = props.globals.initNode("sim/model/rain/flow-mps", 0);
954
955                 var canopyN = props.globals.initNode("gear/canopy/position-norm", 0);
956                 var thresholdN = props.globals.initNode("sim/model/rain/flow-threshold-kt", 15);
957
958                 setlistener(canopyN, func(n) me.canopy = n.getValue(), 1, 0);
959                 setlistener(thresholdN, func(n) me.threshold = n.getValue(), 1);
960                 setlistener("sim/rendering/precipitation-gui-enable", func(n) me.enabled = n.getValue(), 1);
961                 setlistener("environment/metar/rain-norm", func(n) me.rain = n.getValue(), 1);
962                 setlistener("sim/current-view/internal", func(n) me.internal = n.getValue(), 1);
963         },
964         update: func {
965                 var altitude = me.altitudeN.getValue();
966                 var precip_level = me.precip_levelN.getValue();
967
968                 if (me.enabled and me.internal and altitude < precip_level and me.canopy < 0.001) {
969                         var time = me.elapsed_timeN.getValue();
970                         var ias = me.iasN.getValue();
971                         var dt = me.dtN.getValue();
972
973                         me.flowN.setDoubleValue(ias < me.threshold ? 0 : time * 0.5 + ias * NM2M * dt / 3600);
974                         me.rainingN.setDoubleValue(me.rain);
975                         if (me.enableN.getBoolValue())
976                                 me.enableN.setBoolValue(0);
977                 } else {
978                         me.flowN.setDoubleValue(0);
979                         me.rainingN.setDoubleValue(0);
980                         if (me.enableN.getBoolValue() != 1)
981                                 me.enableN.setBoolValue(1);
982                 }
983         },
984 };
985
986
987
988 # teleport
989 # =============================================================================
990 # Usage:  aircraft.teleport(lat:48.3, lon:32.4, alt:5000);
991 #
992 var teleport = func(airport = "", runway = "", lat = -9999, lon = -9999, alt = 0,
993                 speed = 0, distance = 0, azimuth = 0, glideslope = 0, heading = 9999) {
994         setprop("/sim/presets/airport-id", airport);
995         setprop("/sim/presets/runway", runway);
996         setprop("/sim/presets/parkpos", "");
997         setprop("/sim/presets/latitude-deg", lat);
998         setprop("/sim/presets/longitude-deg", lon);
999         setprop("/sim/presets/altitude-ft", alt);
1000         setprop("/sim/presets/airspeed-kt", speed);
1001         setprop("/sim/presets/offset-distance-nm", distance);
1002         setprop("/sim/presets/offset-azimuth-nm", azimuth);
1003         setprop("/sim/presets/glideslope-deg", glideslope);
1004         setprop("/sim/presets/heading-deg", heading);
1005         fgcommand("reposition");
1006 }
1007
1008
1009
1010 # returns wind speed [kt] from given direction [deg]; useful for head-wind
1011 #
1012 var wind_speed_from = func(azimuth) {
1013         var dir = (getprop("/environment/wind-from-heading-deg") - azimuth) * D2R;
1014         return getprop("/environment/wind-speed-kt") * math.cos(dir);
1015 }
1016
1017
1018
1019 # returns true airspeed for given indicated airspeed [kt] and altitude [m]
1020 #
1021 var kias_to_ktas = func(kias, altitude) {
1022         var seapress = getprop("/environment/pressure-sea-level-inhg");
1023         var seatemp = getprop("/environment/temperature-sea-level-degc");
1024         var coralt_ft = altitude * M2FT + (29.92 - seapress) * 910;
1025         return kias * (1 + 0.00232848233 * (seatemp - 15))
1026                         * (1.0025 + coralt_ft * (0.0000153
1027                         - kias * (coralt_ft * 0.0000000000003 + 0.0000000045)
1028                         + (0.0000119 * (math.exp(coralt_ft * 0.000016) - 1))));
1029 }
1030
1031
1032
1033 # HUD control class to handle both HUD implementations
1034 # ==============================================================================
1035 #
1036 var HUD = {
1037         init: func {
1038                 me.vis1N = props.globals.getNode("/sim/hud/visibility[1]", 1);
1039                 me.currcolN = props.globals.getNode("/sim/hud/current-color", 1);
1040                 me.currentPathN = props.globals.getNode("/sim/hud/current-path", 1);
1041                 me.hudN = props.globals.getNode("/sim/hud", 1);
1042                 me.paletteN = props.globals.getNode("/sim/hud/palette", 1);
1043                 me.brightnessN = props.globals.getNode("/sim/hud/color/brightness", 1);
1044                 me.currentN = me.vis1N;
1045                 
1046                 # keep compatibility with earlier version of FG - hud/path[1] is
1047                 # the default Hud
1048                 me.currentPathN.setIntValue(1);
1049         },
1050         cycle_color: func {             # h-key
1051                 if (!me.currentN.getBoolValue())                # if off, turn on
1052                         return me.currentN.setBoolValue(1);
1053
1054                 var i = me.currcolN.getValue() + 1;             # if through, turn off
1055                 if (i < 0 or i >= size(me.paletteN.getChildren("color"))) {
1056                         me.currentN.setBoolValue(0);
1057                         me.currcolN.setIntValue(0);
1058                 } else {                                        # otherwise change color
1059                         me.currentN.setBoolValue(1);
1060                         me.currcolN.setIntValue(i);
1061                 }
1062         },
1063         cycle_brightness: func {        # H-key
1064                 me.is_active() or return;
1065                 var br = me.brightnessN.getValue() - 0.2;
1066                 me.brightnessN.setValue(br > 0.01 ? br : 1);
1067         },
1068     normal_type: func {         # i-key
1069             me.currentPathN.setIntValue(1);
1070     },
1071     cycle_type: func {          # I-key
1072             var i = me.currentPathN.getValue() + 1;     
1073                 if (i < 1 or i > size(me.hudN.getChildren("path"))) {
1074                     # back to the start
1075                         me.currentPathN.setIntValue(1);
1076                 } else {        
1077                         me.currentPathN.setIntValue(i);
1078                 }
1079     },
1080         is_active: func {
1081                 me.vis1N.getValue();
1082         },
1083 };
1084
1085 # crossfeed_valve
1086 # =============================================================================
1087 # class that creates a fuel tank cross-feed valve. Designed for YASim aircraft;
1088 # JSBSim aircraft can simply use systems code within the FDM (see 747-400 for
1089 # an example).
1090 #
1091 # WARNING: this class requires the tank properties to be ready, so call new()
1092 # after the FDM is initialized.
1093 #
1094 # SYNOPSIS:
1095 #       crossfeed_valve.new(<max_flow_rate>, <property>, <tank>, <tank>, ... );
1096 #       crossfeed_valve.open(<update>);
1097 #       crossfeed_valve.close(<update>);
1098 #
1099 #       <max_flow_rate> ... maximum transfer rate between the tanks in lbs/sec
1100 #       <property>      ... property path to use as switch - pass nil to use no such switch
1101 #       <tank>          ... number of a tank to connect - can have unlimited number of tanks connected
1102 #       <update>        ... update switch property when opening/closing valve via Nasal - 0 or 1; by default, 1
1103 #
1104 #
1105 # EXAMPLES:
1106 #       aircraft.crossfeed_valve.new(0.5, "/controls/fuel/x-feed", 0, 1, 2);
1107 #-------
1108 #       var xfeed = aircraft.crossfeed_valve.new(1, nil, 0, 1);
1109 #       xfeed.open();
1110 #
1111 var crossfeed_valve = {
1112         new: func(flow_rate, path) {
1113                 var m = { parents: [crossfeed_valve] };
1114                 m.valve_open = 0;
1115                 m.interval = 0.5;
1116                 m.loopid = -1;
1117                 m.flow_rate = flow_rate;
1118                 if (path != nil) {
1119                         m.switch_node = props.globals.initNode(path, 0, "BOOL");
1120                         setlistener(path, func(node) {
1121                                 if (node.getBoolValue()) m.open(0);
1122                                 else m.close(0);
1123                         }, 1, 0);
1124                 }
1125                 m.tanks = [];
1126                 for (var i = 0; i < size(arg); i += 1) {
1127                         var tank = props.globals.getNode("consumables/fuel/tank[" ~ arg[i] ~ "]");
1128                         if (tank.getChild("level-lbs") != nil) append(m.tanks, tank);
1129                 }
1130                 return m;
1131         },
1132         open: func(update_prop = 1) {
1133                 if (me.valve_open == 1) return;
1134                 if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(1);
1135                 me.valve_open = 1;
1136                 me.loopid += 1;
1137                 settimer(func me._loop_(me.loopid), me.interval);
1138         },
1139         close: func(update_prop = 1) {
1140                 if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(0);
1141                 me.valve_open = 0;
1142         },
1143         _loop_: func(id) {
1144                 if (id != me.loopid) return;
1145                 var average_level = 0;
1146                 var count = size(me.tanks);
1147                 for (var i = 0; i < count; i += 1) {
1148                         var level_node = me.tanks[i].getChild("level-lbs");
1149                         average_level += level_node.getValue();
1150                 }
1151                 average_level /= size(me.tanks);
1152                 var highest_diff = 0;
1153                 for (var i = 0; i < count; i += 1) {
1154                         var level = me.tanks[i].getChild("level-lbs").getValue();
1155                         var diff = math.abs(average_level - level);
1156                         if (diff > highest_diff) highest_diff = diff;
1157                 }
1158                 for (var i = 0; i < count; i += 1) {
1159                         var level_node = me.tanks[i].getChild("level-lbs");
1160                         var capacity = me.tanks[i].getChild("capacity-gal_us").getValue() * me.tanks[i].getChild("density-ppg").getValue();
1161                         var diff = math.abs(average_level - level_node.getValue());
1162                         var min_level = math.max(0, level_node.getValue() - me.flow_rate * diff / highest_diff);
1163                         var max_level = math.min(capacity, level_node.getValue() + me.flow_rate * diff / highest_diff);
1164                         var level = level_node.getValue() > average_level ? math.max(min_level, average_level) : math.min(max_level, average_level);
1165                         level_node.setValue(level);
1166                 }
1167                 if (me.valve_open) settimer(func me._loop_(id), me.interval);
1168         }
1169 };
1170
1171
1172
1173
1174 # module initialization
1175 # ==============================================================================
1176 #
1177 _setlistener("/sim/signals/nasal-dir-initialized", func {
1178         props.globals.initNode("/sim/time/elapsed-sec", 0);
1179         props.globals.initNode("/sim/time/delta-sec", 0);
1180         props.globals.initNode("/sim/time/delta-realtime-sec", 0.00000001);
1181
1182         HUD.init();
1183         data.init();
1184         autotrim.init();
1185
1186 ##### temporary hack to provide backward compatibility for /sim/auto-coordination
1187 ##### remove this code when all references to /sim/auto-coordination are gone
1188         var ac = props.globals.getNode("/sim/auto-coordination");
1189         if(ac != nil ) {
1190                 printlog("alert", 
1191                         "WARNING: using deprecated property /sim/auto-coordination. Please change to /controls/flight/auto-coordination" );
1192                 ac.alias(props.globals.getNode("/controls/flight/auto-coordination", 1));
1193         }
1194 #### end of temporary hack for /sim/auto-coordination
1195
1196         if (!getprop("/sim/startup/restore-defaults")) {
1197                 # load user-specific aircraft settings
1198                 data.load();
1199                 var n = props.globals.getNode("/sim/aircraft-data");
1200                 if (n != nil)
1201                         foreach (var c; n.getChildren("path"))
1202                                 if (c.getType() != "NONE")
1203                                         data.add(c.getValue());
1204         }
1205         if (!getprop("/sim/startup/save-on-exit"))
1206         {
1207                 # prevent saving
1208                 data._save_ = func nil;
1209                 data._loop_ = func nil;
1210         }
1211 });
1212