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#
12var 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#
22var 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#
48var 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#
134var 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 (isscalar(arg[c])) {
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#
252var 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");
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#
286var 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#
346var 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._timer = maketimer(60, me, me._save_);
352
353		setlistener("/sim/signals/reinit", func(n) {
354            if (n.getBoolValue() and getprop("/sim/startup/save-on-exit")) {
355                me._save_();
356            }
357        });
358		setlistener("/sim/signals/exit", func {
359            if (getprop("/sim/startup/save-on-exit")) {
360                me._save_();
361            }
362        });
363	},
364
365	load: func {
366		if (io.stat(me.path) != nil) {
367			logprint(LOG_INFO, "loading aircraft data from ", me.path);
368			io.read_properties(me.path, props.globals);
369		}
370	},
371
372	save: func(v = nil) {
373		if (v == nil) {
374			me._save_();
375            me._timer.stop();
376		} else {
377			me._timer.restart(60 * v);
378		}
379	},
380
381	_save_: func {
382		size(me.catalog) or return;
383		logprint(LOG_INFO, "saving aircraft data to ", me.path);
384		me.signalN.setBoolValue(1);
385		var data = props.Node.new();
386		foreach (var c; me.catalog) {
387			if (c[0] == `/`) {
388				c = substr(c, 1);
389            }
390			props.copy(props.globals.getNode(c, 1), data.getNode(c, 1));
391		}
392		io.write_properties(me.path, data);
393	},
394
395	add: func(p...) {
396		foreach (var n; props.nodeList(p)) {
397			append(me.catalog, n.getPath());
398        }
399	},
400};
401
402
403
404# timer
405# ==============================================================================
406# class that implements timer that can be started, stopped, reset, and can
407# have its value saved to the aircraft specific data file. Saving the value
408# is done automatically by the aircraft.Data class.
409#
410# SYNOPSIS:
411#	timer.new(<property> [, <resolution:double> [, <save:bool>]])
412#
413#	<property>   ... property path or props.Node hash that holds the timer value
414#	<resolution> ... timer update resolution -- interval in seconds in which the
415#	                 timer property is updated while running (default: 1 s)
416#	<save>       ... bool that defines whether the timer value should be saved
417#	                 and restored next time, as needed for Hobbs meters
418#	                 (default: 1)
419#
420# EXAMPLES:
421#	var hobbs_turbine = aircraft.timer.new("/sim/time/hobbs/turbine[0]", 60);
422#	hobbs_turbine.start();
423#
424#	aircraft.timer.new("/sim/time/hobbs/battery", 60).start();  # anonymous timer
425#
426var timer = {
427	new: func(prop, res = 1, save = 1) {
428		var m = { parents: [timer] };
429		m.node = makeNode(prop);
430		if (m.node.getType() == "NONE")
431			m.node.setDoubleValue(0);
432
433		me.systimeN = props.globals.getNode("/sim/time/elapsed-sec", 1);
434		m.last_systime = nil;
435		m.interval = res;
436		m.loopid = 0;
437		m.running = 0;
438		m.reinitL = setlistener("/sim/signals/reinit", func(n) {
439			if (n.getValue()) {
440				m.stop();
441				m.total = m.node.getValue();
442			} else {
443				m.node.setDoubleValue(m.total);
444			}
445		});
446		if (save) {
447			data.add(m.node);
448			m.saveL = setlistener("/sim/signals/save", func m._save_());
449		} else {
450			m.saveL = nil;
451		}
452		return m;
453	},
454	del: func {
455		me.stop();
456		removelistener(me.reinitL);
457		if (me.saveL != nil)
458			removelistener(me.saveL);
459	},
460	start: func {
461		me.running and return;
462		me.last_systime = me.systimeN.getValue();
463		if (me.interval != nil)
464			me._loop_(me.loopid);
465		me.running = 1;
466		me;
467	},
468	stop: func {
469		me.running or return;
470		me.running = 0;
471		me.loopid += 1;
472		me._apply_();
473		me;
474	},
475	reset: func {
476		me.node.setDoubleValue(0);
477		me.last_systime = me.systimeN.getValue();
478	},
479	_apply_: func {
480		var sys = me.systimeN.getValue();
481		me.node.setDoubleValue(me.node.getValue() + sys - me.last_systime);
482		me.last_systime = sys;
483	},
484	_save_: func {
485		if (me.running)
486			me._apply_();
487	},
488	_loop_: func(id) {
489		id != me.loopid and return;
490		me._apply_();
491		settimer(func me._loop_(id), me.interval);
492	},
493};
494
495
496
497# livery
498# =============================================================================
499# Class that maintains livery XML files (see English Electric Lightning for an
500# example). The last used livery is saved on exit and restored next time. Livery
501# files are regular PropertyList XML files whose properties are copied to the
502# main tree.
503#
504# SYNOPSIS:
505#	livery.init(<livery-dir> [, <name-path> [, <sort-path>]]);
506#
507#	<livery-dir> ... directory with livery XML files, relative to $FG_ROOT
508#	<name-path>  ... property path to the livery name in the livery files
509#	                 and the property tree (default: sim/model/livery/name)
510#	<sort-path>  ... property path to the sort criterion (default: same as
511#	                 <name-path> -- that is: alphabetic sorting)
512#
513# EXAMPLE:
514#	aircraft.livery.init("Aircraft/Lightning/Models/Liveries",
515#	                     "sim/model/livery/variant",
516#	                     "sim/model/livery/index");  # optional
517#
518#	aircraft.livery.dialog.toggle();
519#	aircraft.livery.select("OEBH");
520#	aircraft.livery.next();
521#
522var livery = {
523	init: func(dir, nameprop = "sim/model/livery/name", sortprop = nil) {
524		me.parents = [gui.OverlaySelector.new("Select Livery", dir, nameprop,
525				sortprop, "sim/model/livery/file")];
526		me.dialog = me.parents[0];
527	},
528};
529
530
531
532# livery_update
533# =============================================================================
534# Class for maintaining liveries in MP aircraft. It is used in Nasal code that's
535# embedded in aircraft animation XML files, and checks in intervals whether the
536# parent aircraft has changed livery, in which case it changes the livery
537# in the remote aircraft accordingly. This class is a wrapper for overlay_update.
538#
539# SYNOPSIS:
540#	livery_update.new(<livery-dir> [, <interval:10> [, <func>]]);
541#
542#	<livery-dir> ... directory with livery files, relative to $FG_ROOT
543#	<interval>   ... checking interval in seconds (default: 10)
544#	<func>       ... callback function that's called with the ./sim/model/livery/file
545#	                 contents as argument whenever the livery has changed. This can
546#	                 be used for post-processing.
547#
548# EXAMPLE:
549#	<nasal>
550#		<load>
551#			var livery_update = aircraft.livery_update.new(
552#					"Aircraft/R22/Models/Liveries", 30,
553#					func print("R22 livery update"));
554#		</load>
555#
556#		<unload>
557#			livery_update.stop();
558#		</unload>
559#	</nasal>
560#
561var livery_update = {
562	new: func(liveriesdir, interval = 10.01, callback = nil) {
563		var m = { parents: [livery_update, overlay_update.new()] };
564		m.parents[1].add(liveriesdir, "sim/model/livery/file", callback);
565		m.parents[1].interval = interval;
566		return m;
567	},
568	stop: func {
569		me.parents[1].stop();
570	},
571};
572
573
574
575# overlay_update
576# =============================================================================
577# Class for maintaining overlays in MP aircraft. It is used in Nasal code that's
578# embedded in aircraft animation XML files, and checks in intervals whether the
579# parent aircraft has changed an overlay, in which case it copies the respective
580# overlay to the aircraft's root directory.
581#
582# SYNOPSIS:
583#	livery_update.new();
584#	livery_update.add(<overlay-dir>, <property> [, <callback>]);
585#
586#	<overlay-dir> ... directory with overlay files, relative to $FG_ROOT
587#	<property>    ... MP property where the overlay file name can be found
588#	                  (usually one of the sim/multiplay/generic/string properties)
589#	<callback>    ... callback function that's called with two arguments:
590#	                  the file name (without extension) and the overlay directory
591#
592# EXAMPLE:
593#	<nasal>
594#		<load>
595#			var update = aircraft.overlay_update.new();
596#			update.add("Aircraft/F4U/Models/Logos", "sim/multiplay/generic/string");
597#		</load>
598#
599#		<unload>
600#			update.stop();
601#		</unload>
602#	</nasal>
603#
604var overlay_update = {
605	new: func {
606		var m = { parents: [overlay_update] };
607		m.root = cmdarg();
608		m.data = {};
609		m.interval = 10.01;
610		if (m.root.getName() == "multiplayer")
611			m._loop_();
612		return m;
613	},
614	add: func(path, prop, callback = nil) {
615		var path = path ~ '/';
616		me.data[path] = [me.root.initNode(prop, ""), "",
617				isfunc(callback) ? callback : func nil];
618		return me;
619	},
620	stop: func {
621		me._loop_ = func nil;
622	},
623	_loop_: func {
624		foreach (var path; keys(me.data)) {
625			var v = me.data[path];
626			var file = v[0].getValue();
627			if (file != v[1]) {
628				io.read_properties(path ~ file ~ ".xml", me.root);
629				v[2](v[1] = file, path);
630			}
631		}
632		settimer(func me._loop_(), me.interval);
633	},
634};
635
636
637
638# steering
639# =============================================================================
640# Class that implements differential braking depending on rudder position.
641# Note that this overrides the controls.applyBrakes() wrapper. If you need
642# your own version, then override it again after the steering.init() call.
643#
644# SYNOPSIS:
645#	steering.init([<property> [, <threshold>]]);
646#
647#	<property>  ... property path or props.Node hash that enables/disables
648#	                brake steering (usually bound to the js trigger button)
649#	<threshold> ... defines range (+- threshold) around neutral rudder
650#	                position in which both brakes are applied
651#
652# EXAMPLES:
653#	aircraft.steering.init("/controls/gear/steering", 0.2);
654#	aircraft.steering.init();
655#
656var steering = {
657	init: func(switch = "/controls/gear/brake-steering", threshold = 0.3) {
658		me.threshold = threshold;
659		me.switchN = makeNode(switch);
660		me.switchN.setBoolValue(me.switchN.getBoolValue());
661		me.leftN = props.globals.getNode("/controls/gear/brake-left", 1);
662		me.rightN = props.globals.getNode("/controls/gear/brake-right", 1);
663		me.rudderN = props.globals.getNode("/controls/flight/rudder", 1);
664		me.loopid = 0;
665
666		controls.applyBrakes = func(v, w = 0) {
667			if (w < 0)
668				steering.leftN.setValue(v);
669			elsif (w > 0)
670				steering.rightN.setValue(v);
671			else
672				steering.switchN.setValue(v);
673		}
674		setlistener(me.switchN, func(n) {
675			me.loopid += 1;
676			if (n.getValue())
677				me._loop_(me.loopid);
678			else
679				me.setbrakes(0, 0);
680		}, 1);
681	},
682	_loop_: func(id) {
683		id == me.loopid or return;
684		var rudder = me.rudderN.getValue();
685		if (rudder > me.threshold)
686			me.setbrakes(0, rudder);
687		elsif (rudder < -me.threshold)
688			me.setbrakes(-rudder, 0);
689		else
690			me.setbrakes(1, 1);
691
692		settimer(func me._loop_(id), 0);
693	},
694	setbrakes: func(left, right) {
695		me.leftN.setDoubleValue(left);
696		me.rightN.setDoubleValue(right);
697	},
698};
699
700
701
702# autotrim
703# =============================================================================
704# Singleton class that supports quick trimming and compensates for the lack
705# of resistance/force feedback in most joysticks. Normally the pilot trims such
706# that no real or artificially generated (by means of servo motors and spring
707# preloading) forces act on the stick/yoke and it is in a comfortable position.
708# This doesn't work well on computer joysticks.
709#
710# SYNOPSIS:
711#	autotrim.start();  # on key/button press
712#	autotrim.stop();   # on key/button release (mod-up)
713#
714# USAGE:
715#	(1) move the stick such that the aircraft is in an orientation that
716#	    you want to trim for (forward flight, hover, ...)
717#	(2) press autotrim button and keep it pressed
718#	(3) move stick/yoke to neutral position (center)
719#	(4) release autotrim button
720#
721var autotrim = {
722	init: func {
723		me.elevator = me.Trim.new("elevator");
724		me.aileron = me.Trim.new("aileron");
725		me.rudder = me.Trim.new("rudder");
726		me.loopid = 0;
727		me.active = 0;
728	},
729	start: func {
730		me.active and return;
731		me.active = 1;
732		me.elevator.start();
733		me.aileron.start();
734		me.rudder.start();
735		me._loop_(me.loopid += 1);
736	},
737	stop: func {
738		me.active or return;
739		me.active = 0;
740		me.loopid += 1;
741		me.update();
742	},
743	_loop_: func(id) {
744		id == me.loopid or return;
745		me.update();
746		settimer(func me._loop_(id), 0);
747	},
748	update: func {
749		me.elevator.update();
750		me.aileron.update();
751		me.rudder.update();
752	},
753	Trim: {
754		new: func(name) {
755			var m = { parents: [autotrim.Trim] };
756			m.trimN = props.globals.getNode("/controls/flight/" ~ name ~ "-trim", 1);
757			m.ctrlN = props.globals.getNode("/controls/flight/" ~ name, 1);
758			return m;
759		},
760		start: func {
761			me.last = me.ctrlN.getValue();
762		},
763		update: func {
764			var v = me.ctrlN.getValue();
765			me.trimN.setDoubleValue(me.trimN.getValue() + me.last - v);
766			me.last = v;
767		},
768	},
769};
770
771
772
773# tyresmoke
774# =============================================================================
775# Provides a property which can be used to contol particles used to simulate tyre
776# smoke on landing. Weight on wheels, vertical speed, ground speed, ground friction
777# factor are taken into account. Tyre slip is simulated by low pass filters.
778#
779# Modifications to the model file are required.
780#
781# Generic XML particle files are available, but are not mandatory
782# (see Hawker Seahawk for an example).
783#
784# SYNOPSIS:
785#	aircraft.tyresmoke.new(gear index [, auto = 0])
786#		gear index - the index of the gear to which the tyre smoke is attached
787#		auto - enable automatic update (recommended). defaults to 0 for backward compatibility.
788#	aircraft.tyresmoke.del()
789#		destructor.
790#	aircraft.tyresmoke.update()
791#		Runs the update. Not required if automatic updates are enabled.
792#
793# EXAMPLE:
794#	var tyresmoke_0 = aircraft.tyresmoke.new(0);
795#	tyresmoke_0.update();
796#
797# PARAMETERS:
798#
799#    number: index of gear to be animated, i.e. "2" for /gear/gear[2]/...
800#
801#    auto: 1 when tyresmoke should start on update loop. 0 when you're going
802#      to call the update method from one of your own loops.
803#
804#    diff_norm: value adjusting the necessary percental change of roll-speed
805#      to trigger tyre smoke. Default value is 0.05. More realistic results can
806#      be achieved with significantly higher values (i.e. use 0.8).
807#
808#    check_vspeed: 1 when tyre smoke should only be triggered when vspeed is negative
809#      (usually doesn't work for all gear, since vspeed=0.0 after the first gear touches
810#      ground). Use 0 to make tyre smoke independent of vspeed.
811#      Note: in reality, tyre smoke doesn't depend on vspeed, but only on acceleration
812#      and friction.
813#
814
815var tyresmoke = {
816	new: func(number, auto = 0, diff_norm = 0.05, check_vspeed=1) {
817		var m = { parents: [tyresmoke] };
818		m.vertical_speed = (!check_vspeed) ? nil : props.globals.initNode("velocities/vertical-speed-fps");
819		m.diff_norm = diff_norm;
820		m.speed = props.globals.initNode("velocities/groundspeed-kt");
821		m.rain = props.globals.initNode("environment/metar/rain-norm");
822
823		var gear = props.globals.getNode("gear/gear[" ~ number ~ "]/");
824		m.wow = gear.initNode("wow");
825		m.tyresmoke = gear.initNode("tyre-smoke", 0, "BOOL");
826		m.friction_factor = gear.initNode("ground-friction-factor", 1);
827		m.sprayspeed = gear.initNode("sprayspeed-ms");
828		m.spray = gear.initNode("spray", 0, "BOOL");
829		m.spraydensity = gear.initNode("spray-density", 0, "DOUBLE");
830		m.auto = auto;
831		m.listener = nil;
832
833		if (getprop("sim/flight-model") == "jsb") {
834			var wheel_speed = "fdm/jsbsim/gear/unit[" ~ number ~ "]/wheel-speed-fps";
835			m.rollspeed = props.globals.initNode(wheel_speed);
836			m.get_rollspeed = func m.rollspeed.getValue() * 0.3043;
837		} else {
838			m.rollspeed = gear.initNode("rollspeed-ms");
839			m.get_rollspeed = func m.rollspeed.getValue();
840		}
841
842		m.lp = lowpass.new(2);
843		auto and m.update();
844		return m;
845	},
846	del: func {
847		if (me.listener != nil) {
848			removelistener(me.listener);
849			me.listener = nil;
850		}
851		me.auto = 0;
852	},
853	update: func {
854		var rollspeed = me.get_rollspeed();
855		var vert_speed = (me.vertical_speed) != nil ? me.vertical_speed.getValue() : -999;
856		var groundspeed = me.speed.getValue();
857		var friction_factor = me.friction_factor.getValue();
858		var wow = me.wow.getValue();
859		var rain = me.rain.getValue();
860
861		var filtered_rollspeed = me.lp.filter(rollspeed);
862		var diff = math.abs(rollspeed - filtered_rollspeed);
863		var diff_norm = diff > 0 ? diff / rollspeed : 0;
864
865		if (wow and vert_speed < -1.2
866				and diff_norm > me.diff_norm
867				and friction_factor > 0.7 and groundspeed > 50
868				and rain < 0.20) {
869			me.tyresmoke.setValue(1);
870			me.spray.setValue(0);
871			me.spraydensity.setValue(0);
872		} elsif (wow and groundspeed > 5 and rain >= 0.20) {
873			me.tyresmoke.setValue(0);
874			me.spray.setValue(1);
875			me.sprayspeed.setValue(rollspeed * 6);
876			me.spraydensity.setValue(rain * groundspeed);
877		} else {
878			me.tyresmoke.setValue(0);
879			me.spray.setValue(0);
880			me.sprayspeed.setValue(0);
881			me.spraydensity.setValue(0);
882		}
883		if (me.auto) {
884			if (wow) {
885				settimer(func me.update(), 0);
886				if (me.listener != nil) {
887					removelistener(me.listener);
888					me.listener = nil;
889				}
890			} elsif (me.listener == nil) {
891				me.listener = setlistener(me.wow, func me._wowchanged_(), 0, 0);
892			}
893		}
894	},
895	_wowchanged_: func() {
896		if (me.wow.getValue()) {
897			me.lp.set(0);
898			me.update();
899		}
900	},
901};
902
903# tyresmoke_system
904# =============================================================================
905# Helper class to contain the tyresmoke objects for all the gears.
906# Will update automatically, nothing else needs to be done by the caller.
907#
908# SYNOPSIS:
909#	aircraft.tyresmoke_system.new(<gear index 1>, <gear index 2>, ...)
910#		<gear index> - the index of the gear to which the tyre smoke is attached
911#	aircraft.tyresmoke_system.del()
912#		destructor
913# EXAMPLE:
914#	var tyresmoke_system = aircraft.tyresmoke_system.new(0, 1, 2, 3, 4);
915
916var tyresmoke_system = {
917	new: func {
918		var m = { parents: [tyresmoke_system] };
919		# preset array to proper size
920		m.gears = [];
921		setsize(m.gears, size(arg));
922		for(var i = size(arg) - 1; i >= 0; i -= 1) {
923			m.gears[i] = tyresmoke.new(arg[i], 1);
924		}
925		return m;
926	},
927	del: func {
928		foreach(var gear; me.gears) {
929			gear.del();
930		}
931	}
932};
933
934# rain
935# =============================================================================
936# Provides a property which can be used to control rain. Can be used to turn
937# off rain in internal views, and or used with a texture on canopies etc.
938# The output is co-ordinated with system precipitation:
939#
940#	/sim/model/rain/raining-norm  rain intensity
941#	/sim/model/rain/flow-mps      drop flow speed [m/s]
942#
943# See Hawker Seahawk for an example.
944#
945# SYNOPSIS:
946#	aircraft.rain.init();
947#	aircraft.rain.update();
948#
949var rain = {
950	init: func {
951		me.elapsed_timeN = props.globals.getNode("sim/time/elapsed-sec");
952		me.dtN = props.globals.getNode("sim/time/delta-sec");
953
954		me.enableN = props.globals.initNode("sim/rendering/precipitation-aircraft-enable", 0, "BOOL");
955		me.precip_levelN = props.globals.initNode("environment/params/precipitation-level-ft", 0);
956		me.altitudeN = props.globals.initNode("position/altitude-ft", 0);
957		me.iasN = props.globals.initNode("velocities/airspeed-kt", 0);
958		me.rainingN = props.globals.initNode("sim/model/rain/raining-norm", 0);
959		me.flowN = props.globals.initNode("sim/model/rain/flow-mps", 0);
960
961		var canopyN = props.globals.initNode("gear/canopy/position-norm", 0);
962		var thresholdN = props.globals.initNode("sim/model/rain/flow-threshold-kt", 15);
963
964		setlistener(canopyN, func(n) me.canopy = n.getValue(), 1, 0);
965		setlistener(thresholdN, func(n) me.threshold = n.getValue(), 1);
966		setlistener("sim/rendering/precipitation-gui-enable", func(n) me.enabled = n.getValue(), 1);
967		setlistener("environment/metar/rain-norm", func(n) me.rain = n.getValue(), 1);
968		setlistener("sim/current-view/internal", func(n) me.internal = n.getValue(), 1);
969	},
970	update: func {
971		var altitude = me.altitudeN.getValue();
972		var precip_level = me.precip_levelN.getValue();
973
974		if (me.enabled and me.internal and altitude < precip_level and me.canopy < 0.001) {
975			var time = me.elapsed_timeN.getValue();
976			var ias = me.iasN.getValue();
977			var dt = me.dtN.getValue();
978
979			me.flowN.setDoubleValue(ias < me.threshold ? 0 : time * 0.5 + ias * NM2M * dt / 3600);
980			me.rainingN.setDoubleValue(me.rain);
981			if (me.enableN.getBoolValue())
982				me.enableN.setBoolValue(0);
983		} else {
984			me.flowN.setDoubleValue(0);
985			me.rainingN.setDoubleValue(0);
986			if (me.enableN.getBoolValue() != 1)
987				me.enableN.setBoolValue(1);
988		}
989	},
990};
991
992
993
994# teleport
995# =============================================================================
996# Usage:  aircraft.teleport(lat:48.3, lon:32.4, alt:5000);
997#
998var teleport = func(airport = "", runway = "", lat = -9999, lon = -9999, alt = 0,
999		speed = 0, distance = 0, azimuth = 0, glideslope = 0, heading = 9999) {
1000	setprop("/sim/presets/airport-id", airport);
1001	setprop("/sim/presets/runway", runway);
1002	setprop("/sim/presets/parkpos", "");
1003	setprop("/sim/presets/latitude-deg", lat);
1004	setprop("/sim/presets/longitude-deg", lon);
1005	setprop("/sim/presets/altitude-ft", alt);
1006	setprop("/sim/presets/airspeed-kt", speed);
1007	setprop("/sim/presets/offset-distance-nm", distance);
1008	setprop("/sim/presets/offset-azimuth-deg", azimuth);
1009	setprop("/sim/presets/glideslope-deg", glideslope);
1010	setprop("/sim/presets/heading-deg", heading);
1011	fgcommand("reposition");
1012}
1013
1014
1015
1016# returns wind speed [kt] from given direction [deg]; useful for head-wind
1017#
1018var wind_speed_from = func(azimuth) {
1019	var dir = (getprop("/environment/wind-from-heading-deg") - azimuth) * D2R;
1020	return getprop("/environment/wind-speed-kt") * math.cos(dir);
1021}
1022
1023
1024
1025# returns true airspeed for given indicated airspeed [kt] and altitude [m]
1026#
1027var kias_to_ktas = func(kias, altitude) {
1028	var seapress = getprop("/environment/pressure-sea-level-inhg");
1029	var seatemp = getprop("/environment/temperature-sea-level-degc");
1030	var coralt_ft = altitude * M2FT + (29.92 - seapress) * 910;
1031	return kias * (1 + 0.00232848233 * (seatemp - 15))
1032			* (1.0025 + coralt_ft * (0.0000153
1033			- kias * (coralt_ft * 0.0000000000003 + 0.0000000045)
1034			+ (0.0000119 * (math.exp(coralt_ft * 0.000016) - 1))));
1035}
1036
1037
1038
1039# HUD control class to handle both HUD implementations
1040# ==============================================================================
1041#
1042var HUD = {
1043	init: func {
1044		me.vis1N = props.globals.getNode("/sim/hud/visibility[1]", 1);
1045		me.currcolN = props.globals.getNode("/sim/hud/current-color", 1);
1046		me.currentPathN = props.globals.getNode("/sim/hud/current-path", 1);
1047		me.hudN = props.globals.getNode("/sim/hud", 1);
1048		me.paletteN = props.globals.getNode("/sim/hud/palette", 1);
1049		me.brightnessN = props.globals.getNode("/sim/hud/color/brightness", 1);
1050		me.currentN = me.vis1N;
1051
1052		# keep compatibility with earlier version of FG - hud/path[1] is
1053		# the default Hud
1054		me.currentPathN.setIntValue(1);
1055	},
1056	cycle_color: func {		# h-key
1057		if (!me.currentN.getBoolValue())		# if off, turn on
1058			return me.currentN.setBoolValue(1);
1059
1060		var i = me.currcolN.getValue() + 1;		# if through, turn off
1061		if (i < 0 or i >= size(me.paletteN.getChildren("color"))) {
1062			me.currentN.setBoolValue(0);
1063			me.currcolN.setIntValue(0);
1064		} else {					# otherwise change color
1065			me.currentN.setBoolValue(1);
1066			me.currcolN.setIntValue(i);
1067		}
1068	},
1069	cycle_brightness: func {	# H-key
1070		me.is_active() or return;
1071		var br = me.brightnessN.getValue() - 0.2;
1072		me.brightnessN.setValue(br > 0.01 ? br : 1);
1073	},
1074    normal_type: func {		# i-key
1075	    me.currentPathN.setIntValue(1);
1076    },
1077    cycle_type: func {		# I-key
1078	    var i = me.currentPathN.getValue() + 1;
1079		if (i < 1 or i > size(me.hudN.getChildren("path"))) {
1080		    # back to the start
1081			me.currentPathN.setIntValue(1);
1082		} else {
1083			me.currentPathN.setIntValue(i);
1084		}
1085    },
1086	is_active: func {
1087		me.vis1N.getValue();
1088	},
1089};
1090
1091# crossfeed_valve
1092# =============================================================================
1093# class that creates a fuel tank cross-feed valve. Designed for YASim aircraft;
1094# JSBSim aircraft can simply use systems code within the FDM (see 747-400 for
1095# an example).
1096#
1097# WARNING: this class requires the tank properties to be ready, so call new()
1098# after the FDM is initialized.
1099#
1100# SYNOPSIS:
1101#	crossfeed_valve.new(<max_flow_rate>, <property>, <tank>, <tank>, ... );
1102#	crossfeed_valve.open(<update>);
1103#	crossfeed_valve.close(<update>);
1104#
1105#	<max_flow_rate>	... maximum transfer rate between the tanks in lbs/sec
1106#	<property>	... property path to use as switch - pass nil to use no such switch
1107#	<tank>		... number of a tank to connect - can have unlimited number of tanks connected
1108#	<update>	... update switch property when opening/closing valve via Nasal - 0 or 1; by default, 1
1109#
1110#
1111# EXAMPLES:
1112#	aircraft.crossfeed_valve.new(0.5, "/controls/fuel/x-feed", 0, 1, 2);
1113#-------
1114#	var xfeed = aircraft.crossfeed_valve.new(1, nil, 0, 1);
1115#	xfeed.open();
1116#
1117var crossfeed_valve = {
1118	new: func(flow_rate, path) {
1119		var m = { parents: [crossfeed_valve] };
1120		m.valve_open = 0;
1121		m.interval = 0.5;
1122		m.loopid = -1;
1123		m.flow_rate = flow_rate;
1124		if (path != nil) {
1125			m.switch_node = props.globals.initNode(path, 0, "BOOL");
1126			setlistener(path, func(node) {
1127				if (node.getBoolValue()) m.open(0);
1128				else m.close(0);
1129			}, 1, 0);
1130		}
1131		m.tanks = [];
1132		for (var i = 0; i < size(arg); i += 1) {
1133			var tank = props.globals.getNode("consumables/fuel/tank[" ~ arg[i] ~ "]");
1134			if (tank.getChild("level-lbs") != nil) append(m.tanks, tank);
1135		}
1136		return m;
1137	},
1138	open: func(update_prop = 1) {
1139		if (me.valve_open == 1) return;
1140		if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(1);
1141		me.valve_open = 1;
1142		me.loopid += 1;
1143		settimer(func me._loop_(me.loopid), me.interval);
1144	},
1145	close: func(update_prop = 1) {
1146		if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(0);
1147		me.valve_open = 0;
1148	},
1149	_loop_: func(id) {
1150		if (id != me.loopid) return;
1151		var average_level = 0;
1152		var count = size(me.tanks);
1153		for (var i = 0; i < count; i += 1) {
1154			var level_node = me.tanks[i].getChild("level-lbs");
1155			average_level += level_node.getValue();
1156		}
1157		average_level /= size(me.tanks);
1158		var highest_diff = 0;
1159		for (var i = 0; i < count; i += 1) {
1160			var level = me.tanks[i].getChild("level-lbs").getValue();
1161			var diff = math.abs(average_level - level);
1162			if (diff > highest_diff) highest_diff = diff;
1163		}
1164		for (var i = 0; i < count; i += 1) {
1165			var level_node = me.tanks[i].getChild("level-lbs");
1166			var capacity = me.tanks[i].getChild("capacity-gal_us").getValue() * me.tanks[i].getChild("density-ppg").getValue();
1167			var diff = math.abs(average_level - level_node.getValue());
1168			var min_level = math.max(0, level_node.getValue() - me.flow_rate * diff / highest_diff);
1169			var max_level = math.min(capacity, level_node.getValue() + me.flow_rate * diff / highest_diff);
1170			var level = level_node.getValue() > average_level ? math.max(min_level, average_level) : math.min(max_level, average_level);
1171			level_node.setValue(level);
1172		}
1173		if (me.valve_open) settimer(func me._loop_(id), me.interval);
1174	}
1175};
1176
1177props.globals.initNode("/sim/time/elapsed-sec", 0);
1178props.globals.initNode("/sim/time/delta-sec", 0);
1179props.globals.initNode("/sim/time/delta-realtime-sec", 0.00000001);
1180
1181HUD.init();
1182data.init();
1183autotrim.init();
1184
1185##### temporary hack to provide backward compatibility for /sim/auto-coordination
1186##### remove this code when all references to /sim/auto-coordination are gone
1187var ac = props.globals.getNode("/sim/auto-coordination");
1188if (ac != nil) {
1189    logprint(LOG_ALERT, "WARNING: using deprecated property "~
1190    "/sim/auto-coordination. Please change to /controls/flight/auto-coordination");
1191    ac.alias(props.globals.getNode("/controls/flight/auto-coordination", 1));
1192}
1193#### end of temporary hack for /sim/auto-coordination
1194
1195if (!getprop("/sim/startup/restore-defaults")) {
1196    # load user-specific aircraft settings
1197    data.load();
1198    var n = props.globals.getNode("/sim/aircraft-data");
1199    if (n != nil) {
1200        foreach (var c; n.getChildren("path")) {
1201            if (c.getType() != "NONE")
1202            data.add(c.getValue());
1203        }
1204    }
1205}
1206