1
0
Fork 0

Merge branch 'master' of gitorious.org:fg/fgdata

This commit is contained in:
Hal V. Engel 2014-06-14 23:09:34 -07:00
commit 0ee1f09350
558 changed files with 31631 additions and 2874 deletions

View file

@ -0,0 +1,7 @@
# Aggregation of all tests for the Failure Manager
io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_cycle_counter.nas");
io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_altitude_trigger.nas");
io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_mcbf_trigger.nas");
io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_mtbf_trigger.nas");
io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_failure_mode.nas");

View file

@ -0,0 +1,155 @@
# AltitudeTrigger unit tests
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
io.include("Aircraft/Generic/Systems/Tests/test.nas");
io.include("Aircraft/Generic/Systems/failures.nas");
var TestAltitudeTrigger = {
parents: [TestSuite],
setup: func {
props.globals.initNode("/test");
},
cleanup: func {
me.trigger = nil;
props.globals.getNode("/test").remove();
},
test_binding: func {
setprop("/test/foreign-property", 25);
me.trigger = AltitudeTrigger.new(100, 200);
me.trigger.bind("/test/");
assert_prop_exists("/test/reset");
assert_prop_exists("/test/min-altitude-ft");
assert_prop_exists("/test/min-altitude-ft");
me.trigger.unbind();
fail_if_prop_exists("/test/reset");
fail_if_prop_exists("/test/min-altitude-ft");
fail_if_prop_exists("/test/min-altitude-ft");
assert_prop_exists("/test/foreign-property");
},
test_props_are_read_on_reset: func {
me.trigger = AltitudeTrigger.new(100, 200);
me.trigger.bind("/test/");
assert(me.trigger.params["min-altitude-ft"] == 100);
assert(me.trigger.params["max-altitude-ft"] == 200);
setprop("/test/min-altitude-ft", 1000);
setprop("/test/max-altitude-ft", 2000);
assert(me.trigger.params["min-altitude-ft"] == 100);
assert(me.trigger.params["max-altitude-ft"] == 200);
me.trigger.reset();
assert(me.trigger.params["min-altitude-ft"] == 1000);
assert(me.trigger.params["max-altitude-ft"] == 2000);
},
test_trigger_fires_within_min_and_max: func {
me.trigger = AltitudeTrigger.new(100, 200);
me.trigger._altitude_prop = "/test/fake-altitude-ft";
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 0);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 300);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 150);
assert(me.trigger.update() == 1);
assert(me.trigger.fired);
},
test_trigger_accepts_nil_max: func {
me.trigger = AltitudeTrigger.new(500, nil);
me.trigger._altitude_prop = "/test/fake-altitude-ft";
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", -250);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 0);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 250);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 750);
assert(me.trigger.update() == 1);
assert(me.trigger.fired);
},
test_trigger_accepts_nil_min: func {
me.trigger = AltitudeTrigger.new(nil, 500);
me.trigger._altitude_prop = "/test/fake-altitude-ft";
me.trigger.bind("/test/trigger/");
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 750);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 500);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-altitude-ft", 250);
assert(me.trigger.update() == 1);
assert(me.trigger.fired);
me.trigger.reset();
setprop("/test/fake-altitude-ft", -250);
assert(me.trigger.update() == 1);
assert(me.trigger.fired);
},
test_trigger_dies_if_both_params_are_nil: func {
call(AltitudeTrigger.new, [nil, nil], AltitudeTrigger, var err = []);
assert(size(err) > 0);
},
test_to_str: func {
me.trigger = AltitudeTrigger.new(100, 200);
call(me.trigger.to_str, [], me.trigger, var err = []);
assert(size(err) == 0);
}
};

View file

@ -0,0 +1,124 @@
# CycleCounter unit tests
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
io.include("Aircraft/Generic/Systems/Tests/test.nas");
io.include("Aircraft/Generic/Systems/failures.nas");
var TestCycleCounter = {
parents: [TestSuite],
setup: func {
props.globals.initNode("/test");
},
cleanup: func {
props.globals.getNode("/test").remove();
me.counter = nil;
},
_shake_that_prop: func (pattern=nil) {
if (pattern == nil)
pattern = [0, -10, 10, -10, 10, -10, 10, 0];
setprop("/test/property", pattern[0]);
me.counter.reset();
var i = 0;
var value = pattern[0];
var target = pattern[1];
var delta = 0;
while(i < size(pattern) - 1) {
target = pattern[i+1];
delta = pattern[i+1] > pattern[i] ? 1 : -1;
while(value != target) {
value += delta;
setprop("/test/property", value);
}
i += 1;
}
},
test_cycles_dont_grow_while_disabled: func {
me.counter = CycleCounter.new("/test/property");
me._shake_that_prop();
assert(me.counter.cycles == 0);
},
test_cycles_grow_while_enabled: func {
me.counter = CycleCounter.new("/test/property");
me._shake_that_prop();
assert(me.counter.cycles == 0);
me.counter.enable();
me._shake_that_prop();
assert(me.counter.cycles == 3);
},
test_reset: func {
me.counter = CycleCounter.new("/test/property");
me.counter.enable();
me._shake_that_prop();
assert(me.counter.cycles > 0);
me.counter.reset();
assert(me.counter.cycles == 0);
},
test_callback_every_half_cycle: func {
var count = 0;
me.counter = CycleCounter.new(
property: "/test/property",
on_update: func (cycles) { count += 1 });
me.counter.enable();
me._shake_that_prop();
assert(count == 6);
},
test_callback_reports_cycle_count: func {
var count = 0;
var cb = func (cycles) {
count += 1;
assert(cycles == count * 0.5);
};
me.counter = CycleCounter.new(
property: "/test/property", on_update: cb);
me.counter.enable();
me._shake_that_prop();
},
test_counter_works_for_binary_props: func {
me.counter = CycleCounter.new("/test/property");
me.counter.enable();
me._shake_that_prop([0, 1, 0, 1, 0, 1]);
assert(me.counter.cycles == 2);
}
};

View file

@ -0,0 +1,113 @@
# AltitudeTrigger unit tests
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
io.include("Aircraft/Generic/Systems/Tests/test.nas");
io.include("Aircraft/Generic/Systems/failures.nas");
var TestFailureMode = {
parents: [TestSuite],
setup: func {
props.globals.initNode("/test");
},
cleanup: func {
me.mode = nil;
props.globals.getNode("/test").remove();
},
test_binding: func {
var actuator = { parents: [FailureMgr.FailureActuator] };
setprop("/test/foreign-property", 25);
me.mode = FailureMgr.FailureMode.new(
id: "instruments/compass",
description: "a description",
actuator: actuator);
me.mode.bind("/test/");
assert_prop_exists("/test/instruments/compass/failure-level");
me.mode.unbind();
fail_if_prop_exists("/test/instruments/compass/failure-level");
fail_if_prop_exists("/test/instruments/compass");
assert_prop_exists("/test/foreign-property");
},
test_set_failure_level_calls_actuator: func {
var level = 0;
var actuator = {
parents: [FailureMgr.FailureActuator],
set_failure_level: func (l) { level = l },
};
me.mode = FailureMgr.FailureMode.new(
id: "instruments/compass",
description: "a description",
actuator: actuator);
me.mode.bind("/test/");
me.mode.set_failure_level(1);
assert(level == 1);
},
test_actuator_gets_called_from_prop: func {
var level = 0;
var actuator = {
parents: [FailureMgr.FailureActuator],
set_failure_level: func (l) { level = l },
};
me.mode = FailureMgr.FailureMode.new(
id: "instruments/compass",
description: "a description",
actuator: actuator);
me.mode.bind("/test/");
setprop("/test/instruments/compass/failure-level", 1);
assert(level == 1);
},
test_setting_level_from_nasal_is_shown_in_prop: func {
var level = 0;
var actuator = {
parents: [FailureMgr.FailureActuator],
set_failure_level: func (l) { level = l },
};
me.mode = FailureMgr.FailureMode.new(
id: "instruments/compass",
description: "a description",
actuator: actuator);
me.mode.bind("/test/");
me.mode.set_failure_level(1);
assert(level == 1);
var prop_value = getprop("/test/instruments/compass/failure-level");
assert(prop_value == 1);
me.mode.set_failure_level(0.5);
assert(level == 0.5);
prop_value = getprop("/test/instruments/compass/failure-level");
assert(prop_value == 0.5);
}
};

View file

@ -0,0 +1,116 @@
# McbfTrigger unit tests
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
io.include("Aircraft/Generic/Systems/Tests/test.nas");
io.include("Aircraft/Generic/Systems/failures.nas");
var TestMcbfTrigger = {
parents: [TestSuite],
setup: func {
props.globals.initNode("/test");
},
cleanup: func {
me.trigger = nil;
props.globals.getNode("/test").remove();
},
_do_one_cycle: func (prop) {
setprop(prop, 10);
setprop(prop, -10);
setprop(prop, 0);
},
test_binding: func {
setprop("/test/property", 0);
me.trigger = McbfTrigger.new("/test/property", 3);
me.trigger.bind("/test/");
assert_prop_exists("/test/reset");
assert_prop_exists("/test/mcbf");
me.trigger.unbind();
fail_if_prop_exists("/test/reset");
fail_if_prop_exists("/test/mcbf");
assert_prop_exists("/test/property");
},
test_trigger_fires_after_activation_cycles: func {
setprop("/test/property", 25);
me.trigger = McbfTrigger.new("/test/property", 3);
me.trigger.activation_cycles = 3;
me.trigger.enable();
assert(!me.trigger.fired);
for (var i = 1; i < 5; i += 1) {
me._do_one_cycle("/test/property");
assert(me.trigger.fired == (i > 3));
}
},
test_trigger_notifies_observer_once: func {
var observer_called = 0;
var on_fire = func observer_called += 1;
setprop("/test/property", 25);
me.trigger = McbfTrigger.new("/test/property", 3);
me.trigger.activation_cycles = 3;
me.trigger.on_fire = on_fire;
me.trigger.enable();
assert(!me.trigger.fired);
for (var i = 1; i < 5; i += 1)
me._do_one_cycle("/test/property");
assert(observer_called == 1);
},
test_reset: func {
setprop("/test/property", 25);
me.trigger = McbfTrigger.new("/test/property", 3);
me.trigger.activation_cycles = 3;
me.trigger.bind("/test");
me.trigger.enable();
for (var i = 1; i < 5; i += 1)
me._do_one_cycle("/test/property");
assert(me.trigger.fired);
me.trigger.reset();
me.trigger.activation_cycles = 3;
assert(!me.trigger.fired);
for (var i = 1; i < 5; i += 1) {
me._do_one_cycle("/test/property");
assert(me.trigger.fired == (i > 3));
}
},
test_to_str: func {
me.trigger = McbfTrigger.new("/test/property", 3);
call(me.trigger.to_str, [], me.trigger, var err = []);
assert(size(err) == 0);
}
};

View file

@ -0,0 +1,87 @@
# MtbfTrigger unit tests
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
io.include("Aircraft/Generic/Systems/Tests/test.nas");
io.include("Aircraft/Generic/Systems/failures.nas");
var TestMtbfTrigger = {
parents: [TestSuite],
setup: func {
props.globals.initNode("/test");
},
cleanup: func {
me.trigger = nil;
props.globals.getNode("/test").remove();
},
test_binding: func {
setprop("/test/foreign-property", 25);
me.trigger = MtbfTrigger.new(60);
me.trigger.bind("/test/");
assert_prop_exists("/test/reset");
assert_prop_exists("/test/mtbf");
me.trigger.unbind();
fail_if_prop_exists("/test/reset");
fail_if_prop_exists("/test/mtbf");
assert_prop_exists("/test/foreign-property");
},
test_props_are_read_on_reset: func {
me.trigger = MtbfTrigger.new(60);
me.trigger.bind("/test/");
assert(me.trigger.params["mtbf"] == 60);
setprop("/test/mtbf", 120);
assert(me.trigger.params["mtbf"] == 60);
me.trigger.reset();
assert(me.trigger.params["mtbf"] == 120);
},
test_trigger_fires_after_fire_time: func {
me.trigger = MtbfTrigger.new(60);
me.trigger._time_prop = "/test/fake-time-sec";
me.trigger.fire_time = 60;
assert(!me.trigger.fired);
setprop("/test/fake-time-sec", 50);
assert(me.trigger.update() == 0);
assert(!me.trigger.fired);
setprop("/test/fake-time-sec", 70);
assert(me.trigger.update() == 1);
assert(me.trigger.fired);
},
test_to_str: func {
me.trigger = MtbfTrigger.new(60);
call(me.trigger.to_str, [], me.trigger, var err = []);
assert(size(err) == 0);
}
};

View file

@ -0,0 +1,139 @@
# Minimalistic framework for automated testing in Nasal
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# TestSuite
#
# Tests are organized in test suites. Each test suite contains an arbitrary
# number of tests, and two special methods "setup" and "cleanup". Setup is
# called before every test, and cleanup is called after every test.
#
# In order to define a test suite, you have to create an object parented to
# TestSuite. The testing framework will identify any method in your object whose
# name starts with "test_" as a test case to be run.
#
# Important: The order in which test cases and test suites are executed
# is undefined!
#
# Example:
#
# var MyTestSuite = {
#
# parents: [TestSuite],
#
# setup: func {
# Stuff to do before every test...
# This is optional. You don't need to provide it if you don't use it.
# },
#
# cleanup: func {
# Stuff to do after every test...
# Also optional.
# },
#
# my_auxiliary_function: func {
# Methods that do not start with "test_" will not be executed by the
# test runner. You can define as many auxiliary functions in the test
# suite as you wish.
# },
#
# test_trivial_test: func {
# This is a real test (starts with "test_"), and it will be run by the
# framework when test_run() is called.#
# }
# };
var TestSuite = {
setup: func 0,
cleanup: func 0
};
# run_tests([namespace])
#
# Executes all test suites found in the given namespace. If no namespace is
# specified, then the namespace where run_tests is defined is used by default.
#
# An effective way to work with the framework is to just include the framework
# from your test files:
#
# io.include(".../test.nas");
#
# and then execute a script like this in the Nasal Console:
#
# delete(globals, "test");
# io.load_nasal(".../my_test_suite.nas", "test");
# test.run_tests();
#
# What this script does is: it empties the "test" namespace and then loads your
# script into that namespace. The test framework will be loaded in there as
# well if it was io.include'd in my_test_suite.nas. Finally, all test suites
# in the "test" namespace are executed.
var run_tests = func(namespace=nil) {
var ns = namespace != nil ? namespace : closure(run_tests, 1);
var passed = 0;
var failed = 0;
var err = [];
foreach(var suite_name; keys(ns)) {
var suite = ns[suite_name];
if (!isa(suite, TestSuite))
continue;
print("Running test suite ", suite_name);
foreach (var test_name; keys(suite)) {
if (find("test_", test_name) != 0)
continue;
# Run the test case
setsize(err, 0);
contains(suite, "setup") and call(suite.setup, [], suite, err);
size(err) == 0 and call(suite[test_name], [], suite, err);
size(err) == 0 and contains(suite, "cleanup") and call(suite.cleanup, [], suite, err);
if (size(err) == 0) {
passed += 1;
continue;
}
failed += 1;
print("Test ", test_name, " FAILED\n");
debug.printerror(err);
}
}
print(sprintf("\n%d tests run. %d passed, %d failed",
passed + failed, passed, failed));
}
var assert_prop_exists = func (prop) {
assert(props.globals.getNode(prop) != nil,
sprintf("Property %s does not exist", prop));
}
var fail_if_prop_exists = func (prop) {
assert(props.globals.getNode(prop) == nil,
sprintf("Property %s exists", prop));
}

View file

@ -0,0 +1,210 @@
# Compatibility failure modes
#
# Loads FailureMgr with the failure modes that where previously hardcoded,
# emulating former behavior and allowing backward compatibility.
#
# Copyright (C) 2014 Anton Gomez Alvedro
# Based on previous work by Stuart Buchanan, Erobo & John Denker
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
io.include("Aircraft/Generic/Systems/failures.nas");
MTBF = 0;
MCBF = 1;
SERV = 0;
JAM = 1;
ENG = 2;
var compat_modes = [
# Instruments
{ id: "instrumentation/adf", type: MTBF, failure: SERV, desc: "ADF" },
{ id: "instrumentation/dme", type: MTBF, failure: SERV, desc: "DME" },
{ id: "instrumentation/airspeed-indicator", type: MTBF, failure: SERV, desc: "ASI" },
{ id: "instrumentation/altimeter", type: MTBF, failure: SERV, desc: "Altimeter" },
{ id: "instrumentation/attitude-indicator", type: MTBF, failure: SERV, desc: "Attitude Indicator" },
{ id: "instrumentation/heading-indicator", type: MTBF, failure: SERV, desc: "Heading Indicator" },
{ id: "instrumentation/magnetic-compass", type: MTBF, failure: SERV, desc: "Magnetic Compass" },
{ id: "instrumentation/nav/gs", type: MTBF, failure: SERV, desc: "Nav 1 Glideslope" },
{ id: "instrumentation/nav/cdi", type: MTBF, failure: SERV, desc: "Nav 1 CDI" },
{ id: "instrumentation/nav[1]/gs", type: MTBF, failure: SERV, desc: "Nav 2 Glideslope" },
{ id: "instrumentation/nav[1]/cdi", type: MTBF, failure: SERV, desc: "Nav 2 CDI" },
{ id: "instrumentation/slip-skid-ball", type: MTBF, failure: SERV, desc: "Slip/Skid Ball" },
{ id: "instrumentation/turn-indicator", type: MTBF, failure: SERV, desc: "Turn Indicator" },
{ id: "instrumentation/vertical-speed-indicator", type: MTBF, failure: SERV, desc: "VSI" },
# Systems
{ id: "systems/electrical", type: MTBF, failure: SERV, desc: "Electrical system" },
{ id: "systems/pitot", type: MTBF, failure: SERV, desc: "Pitot system" },
{ id: "systems/static", type: MTBF, failure: SERV, desc: "Static system" },
{ id: "systems/vacuum", type: MTBF, failure: SERV, desc: "Vacuum system" },
# Controls
{ id: "controls/flight/aileron", type: MTBF, failure: JAM, desc: "Aileron" },
{ id: "controls/flight/elevator", type: MTBF, failure: JAM, desc: "Elevator" },
{ id: "controls/flight/rudder", type: MTBF, failure: JAM, desc: "Rudder" },
{ id: "controls/flight/flaps", type: MCBF, failure: JAM, desc: "Flaps" },
{ id: "controls/flight/speedbrake", type: MCBF, failure: JAM, desc: "Speed Brake" },
{ id: "controls/gear", type: MCBF, failure: SERV, desc: "Gear", prop: "/gear", mcbf_prop: "/controls/gear/gear-down" }
];
##
# Handles the old failures.nas property tree interface,
# sending the appropriate commands to the new FailureMgr.
var compat_listener = func(prop) {
var new_trigger = func {
if (name == "mtbf") {
MtbfTrigger.new(value);
}
else {
var control = id;
forindex(var i; compat_modes) {
var mode = compat_modes[i];
if (mode.id == id and contains(compat_modes[i], "mcbf_prop")) {
control = mode.mcbf_prop;
break;
}
}
McbfTrigger.new(control, value);
}
};
var name = prop.getName();
var value = prop.getValue();
var id = string.replace(io.dirname(prop.getPath()), FailureMgr.proproot, "");
id = string.trim(id, 0, func(c) c == `/`);
if (name == "serviceable") {
FailureMgr.set_failure_level(id, 1 - value);
return;
}
if (name == "failure-level") {
setprop(io.dirname(prop.getPath()) ~ "/serviceable", value ? 0 : 1);
return;
}
# mtbf and mcbf parameter handling
var trigger = FailureMgr.get_trigger(id);
if (value == 0) {
trigger != nil and FailureMgr.set_trigger(id, nil);
return;
}
if (trigger == nil) {
FailureMgr.set_trigger(id, new_trigger());
}
else {
trigger.set_param(name, value);
trigger.reset();
}
}
##
# Called from the ramdom-failures dialog to set the global MCBF parameter
var apply_global_mcbf = func(value) {
foreach (var mode; compat_modes) {
mode.type != MCBF and continue;
setprop(FailureMgr.proproot ~ mode.id ~ "/mcbf", value);
}
}
##
# Called from the ramdom-failures dialog to set the global MTBF parameter
var apply_global_mtbf = func(value) {
foreach (var mode; compat_modes) {
mode.type != MTBF and continue;
setprop(FailureMgr.proproot ~ mode.id ~ "/mtbf", value);
}
}
##
# Discover aircraft engines dynamically and add a failure mode to the
# compat_modes table for each engine.
var populate_engine_data = func {
var engines = props.globals.getNode("/engines");
var engine_id = 0;
foreach (var e; engines.getChildren("engine")) {
var starter = e.getChild("starter");
var running = e.getChild("running");
(starter != nil and starter != "" and starter.getType() != "NONE")
or (running != nil and running != "" and running.getType() != "NONE")
or continue;
var id = "engines/engine";
if (engine_id > 0)
id = id ~ "[" ~ engine_id ~ "]";
var entry = {
id: id,
desc: "Engine " ~ (engine_id + 1),
type: MTBF,
failure: ENG
};
append(compat_modes, entry);
engine_id += 1;
}
}
##
# Subscribes all failure modes that the old failures.nas module did,
# and recreates the same property tree interface (more or less).
var compat_setup = func {
removelistener(lsnr);
populate_engine_data();
foreach (var m; compat_modes) {
var control_prop = contains(m, "prop") ? m.prop : m.id;
FailureMgr.add_failure_mode(
id: m.id,
description: m.desc,
actuator: if (m.failure == SERV) set_unserviceable(control_prop)
elsif (m.failure == JAM) set_readonly(control_prop)
else fail_engine(io.basename(control_prop)));
# Recreate the prop tree interface
var prop = FailureMgr.proproot ~ m.id;
var n = props.globals.initNode(prop ~ "/serviceable", 1, "BOOL");
setlistener(n, compat_listener, 0, 0);
setlistener(prop ~ "/failure-level", compat_listener, 0, 0);
var trigger_type = (m.type == MTBF) ? "/mtbf" : "/mcbf";
setprop(prop ~ trigger_type, 0);
setlistener(prop ~ trigger_type, compat_listener, 0, 0);
}
}
var lsnr = setlistener("sim/signals/fdm-initialized", compat_setup);

View file

@ -0,0 +1,365 @@
# Failure simulation library
#
# Collection of generic Triggers and FailureActuators for programming the
# FailureMgr Nasal module.
#
# Copyright (C) 2014 Anton Gomez Alvedro
# Based on previous work by Stuart Buchanan, Erobo & John Denker
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Functions for generating FailureActuators
# ------------------------------------------
##
# Returns an actuator object that will set the serviceable property at
# the given node to zero when the level of failure is > 0.
var set_unserviceable = func(path) {
var prop = path ~ "/serviceable";
if (props.globals.getNode(prop) == nil)
props.globals.initNode(prop, 1, "BOOL");
return {
parents: [FailureMgr.FailureActuator],
set_failure_level: func(level) setprop(prop, level > 0 ? 0 : 1),
get_failure_level: func { getprop(prop) ? 0 : 1 }
}
}
##
# Returns an actuator object that will make the given property read only.
# This prevents any other system from updating it, and effectively jamming
# whatever it is that is controlling.
var set_readonly = func(property) {
return {
parents: [FailureMgr.FailureActuator],
set_failure_level: func(level) {
var pnode = props.globals.getNode(property);
pnode.setAttribute("writable", level > 0 ? 0 : 1);
},
get_failure_level: func {
var pnode = props.globals.getNode(property);
pnode.getAttribute("writable") ? 0 : 1;
}
}
}
##
# Returns an an actuator object the manipulates engine controls (magnetos &
# cutoff) to simulate an engine failure. Sets these properties to read only
# while the system is failed.
var fail_engine = func(engine) {
return {
parents: [FailureMgr.FailureActuator],
level: 0,
magnetos: props.globals.getNode("/controls/engines/" ~ engine ~ "/magnetos", 1),
cutoff: props.globals.getNode("/controls/engines/" ~ engine ~ "/cutoff", 1),
get_failure_level: func me.level,
set_failure_level: func(level) {
if (level) {
# Switch off the engine, and disable writing to it.
me.magnetos.setValue(0);
me.magnetos.setAttribute("writable", 0);
me.cutoff.setValue(1);
me.cutoff.setAttribute("writable", 0);
}
else {
# Enable the properties, but don't set the magnetos, as they may
# be off for a reason.
me.magnetos.setAttribute("writable", 1);
me.cutoff.setAttribute("writable", 1);
me.cutoff.setValue(0);
}
me.level = level;
}
}
}
#
# Triggers
# ---------
##
# Returns a random number from a Normal distribution with given mean and
# standard deviation.
var norm_rand = func(mean, std) {
var r = -2 * math.ln(1 - rand());
var a = 2 * math.pi * (1 - rand());
return mean + (math.sqrt(r) * math.sin(a) * std);
};
##
# Trigger object that will fire when aircraft altitude is between
# min and max, both specified in feet. One of min or max may be nil for
# expressing "altitude > x" or "altitude < x" conditions.
var AltitudeTrigger = {
parents: [FailureMgr.Trigger],
requires_polling: 1,
new: func(min, max) {
min != nil or max != nil or
die("AltitudeTrigger.new: either min or max must be specified");
var m = FailureMgr.Trigger.new();
m.parents = [AltitudeTrigger];
m.params["min-altitude-ft"] = min;
m.params["max-altitude-ft"] = max;
m._altitude_prop = "/position/altitude-ft";
return m;
},
to_str: func {
# TODO: Handle min or max == nil
sprintf("Altitude between %d and %d ft",
int(me.params["min-altitude-ft"]), int(me.params["max-altitude-ft"]))
},
update: func {
var alt = getprop(me._altitude_prop);
var min = me.params["min-altitude-ft"];
var max = me.params["max-altitude-ft"];
me.fired = min != nil ? min < alt : 1;
me.fired = max != nil ? me.fired and alt < max : me.fired;
}
};
##
# Trigger object that fires when the aircraft's position is within a certain
# distance of a given waypoint.
var WaypointTrigger = {
parents: [FailureMgr.Trigger],
requires_polling: 1,
new: func(lat, lon, distance) {
var wp = geo.Coord.new();
wp.set_latlon(lat, lon);
var m = FailureMgr.Trigger.new();
m.parents = [WaypointTrigger];
m.params["latitude-deg"] = lat;
m.params["longitude-deg"] = lon;
m.params["distance-nm"] = distance;
m.waypoint = wp;
return m;
},
reset: func {
call(FailureMgr.Trigger.reset, [], me);
me.waypoint.set_latlon(me.params["latitude-deg"],
me.params["longitude-deg"]);
},
to_str: func {
sprintf("Within %.2f miles of %s", me.params["distance-nm"],
geo.format(me.waypoint.lat, me.waypoint.lon));
},
update: func {
var d = geo.aircraft_position().distance_to(me.waypoint) * M2NM;
me.fired = d < me.params["distance-nm"];
}
};
##
# Trigger object that will fire on average after the specified time.
var MtbfTrigger = {
parents: [FailureMgr.Trigger],
# TODO: make this trigger async
requires_polling: 1,
new: func(mtbf) {
var m = FailureMgr.Trigger.new();
m.parents = [MtbfTrigger];
m.params["mtbf"] = mtbf;
m.fire_time = 0;
m._time_prop = "/sim/time/elapsed-sec";
return m;
},
reset: func {
call(FailureMgr.Trigger.reset, [], me);
# TODO: use an elapsed time prop that accounts for speed-up and pause
me.fire_time = getprop(me._time_prop)
+ norm_rand(me.params["mtbf"], me.params["mtbf"] / 10);
},
to_str: func {
sprintf("Mean time between failures: %f.1 mins", me.params["mtbf"] / 60);
},
update: func {
me.fired = getprop(me._time_prop) > me.fire_time;
}
};
##
# Trigger object that will fire exactly after the given timeout.
var TimeoutTrigger = {
parents: [FailureMgr.Trigger],
# TODO: make this trigger async
requires_polling: 1,
new: func(timeout) {
var m = FailureMgr.Trigger.new();
m.parents = [TimeoutTrigger];
m.params["timeout-sec"] = timeout;
fire_time = 0;
return m;
},
reset: func {
call(FailureMgr.Trigger.reset, [], me);
# TODO: use an elapsed time prop that accounts for speed-up and pause
me.fire_time = getprop("/sim/time/elapsed-sec")
+ me.params["timeout-sec"];
},
to_str: func {
sprintf("Fixed delay: %d minutes", me.params["timeout-sec"] / 60);
},
update: func {
me.fired = getprop("/sim/time/elapsed-sec") > me.fire_time;
}
};
##
# Simple approach to count usage cycles for a given property. Every time
# the propery variation changes in direction, we count half a cycle.
# If the property represents aileron angular position, for example, this
# would count roughly the number of times the aileron has been actuated.
var CycleCounter = {
new: func(property, on_update = nil) {
return {
parents: [CycleCounter],
cycles: 0,
_property: property,
_on_update: on_update,
_prev_value: getprop(property),
_prev_delta: 0,
_lsnr: nil
};
},
enable: func {
if (me._lsnr == nil)
me._lsnr = setlistener(me._property, func (p) me._on_prop_change(p), 0, 0);
},
disable: func {
if (me._lsnr != nil) removelistener(me._lsnr);
},
reset: func {
me.cycles = 0;
me._prev_value = getprop(me._property);
me._prev_delta = 0;
},
_on_prop_change: func(prop) {
# TODO: Implement a filter for avoiding spureous values.
var value = prop.getValue();
var delta = value - me._prev_value;
if (delta == 0) return;
if (delta * me._prev_delta < 0) {
# Property variation has changed direction
me.cycles += 0.5;
if (me._on_update != nil) me._on_update(me.cycles);
}
me._prev_delta = delta;
me._prev_value = value;
}
};
##
# Trigger object that will fire on average after a property has gone through
# mcbf (mean cycles between failures) cycles.
var McbfTrigger = {
parents: [FailureMgr.Trigger],
requires_polling: 0,
new: func(property, mcbf) {
var m = FailureMgr.Trigger.new();
m.parents = [McbfTrigger];
m.params["mcbf"] = mcbf;
m.counter = CycleCounter.new(property, func(c) call(m._on_cycle, [c], m));
m.activation_cycles = 0;
m.enabled = 0;
return m;
},
enable: func {
me.counter.enable();
me.enabled = 1;
},
disable: func {
me.counter.disable();
me.enabled = 0;
},
reset: func {
call(FailureMgr.Trigger.reset, [], me);
me.counter.reset();
me.activation_cycles =
norm_rand(me.params["mcbf"], me.params["mcbf"] / 10);
me.enabled and me.counter.enable();
},
to_str: func {
sprintf("Mean cycles between failures: %.2f", me.params["mcbf"]);
},
_on_cycle: func(cycles) {
if (!me.fired and cycles > me.activation_cycles) {
# TODO: Why this doesn't work?
# me.counter.disable();
me.fired = 1;
me.on_fire();
}
}
};

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -0,0 +1,327 @@
# Failure Manager implementation
#
# Monitors trigger conditions periodically and fires failure modes when those
# conditions are met. It also provides a central access point for publishing
# failure modes to the user interface and the property tree.
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
##
# Represents one way things can go wrong, for example "a blown tire".
var FailureMode = {
##
# id: Unique identifier for this failure mode.
# eg: "engine/carburetor-ice"
#
# description: Short text description, suitable for printing to the user.
# eg: "Ice in the carburetor"
#
# actuator: Object implementing the FailureActuator interface.
# Used by the failure manager to apply a certain level of
# failure to the failure mode.
new: func(id, description, actuator) {
return {
parents: [FailureMode],
id: id,
description: description,
actuator: actuator,
_path: nil
};
},
##
# Applies a certain level of failure to this failure mode.
# level: Floating point number in the range [0, 1] zero being no failure
# and 1 total failure.
set_failure_level: func(level) {
me._path != nil or
die("FailureMode.set_failure_level: Unbound failure mode");
setprop(me._path ~ me.id ~ "/failure-level", level);
},
##
# Internal version that actually does the job.
_set_failure_level: func(level) {
me.actuator.set_failure_level(level);
me._log_failure(sprintf("%s failure level %d%%",
me.description, level*100));
},
##
# Returns the level of failure currently being simulated.
get_failure_level: func me.actuator.get_failure_level(),
##
# Creates an interface for this failure mode in the property tree at the
# given location. Currently the interface is just:
#
# path/failure-level (double, rw)
bind: func(path) {
me._path == nil or die("FailureMode.bind: mode already bound");
var prop = path ~ me.id ~ "/failure-level";
props.globals.initNode(prop, me.actuator.get_failure_level(), "DOUBLE");
setlistener(prop, func (p) me._set_failure_level(p.getValue()), 0, 0);
me._path = path;
},
##
# Remove bound properties from the property tree.
unbind: func {
me._path != nil and props.globals.getNode(me._path ~ me.id).remove();
me._path = nil;
},
##
# Send a message to the logging facilities, currently the screen and
# the console.
_log_failure: func(message) {
print(getprop("/sim/time/gmt-string") ~ " : " ~ message);
if (getprop(proproot ~ "/display-on-screen"))
screen.log.write(message, 1.0, 0.0, 0.0);
},
};
##
# Implements the FailureMgr functionality.
#
# It is wrapped into an object to leave the door open to several evolution
# approaches, for example moving the implementation down to the C++ engine,
# or supporting several independent instances of the failure manager.
# Additionally, it also serves to isolate low level implementation details
# into its own namespace.
var _failmgr = {
timer: nil,
update_period: 10, # 0.1 Hz
failure_modes: {},
pollable_trigger_count: 0,
init: func {
me.timer = maketimer(me.update_period, func me._update());
setlistener("sim/signals/reinit", func me._on_reinit());
props.globals.initNode(proproot ~ "display-on-screen", 1, "BOOL");
props.globals.initNode(proproot ~ "enabled", 1, "BOOL");
setlistener(proproot ~ "enabled",
func (n) { n.getValue() ? me._enable() : me._disable() });
},
##
# Subscribe a new failure mode to the system.
# mode: FailureMode object.
add_failure_mode: func(mode) {
contains(me.failure_modes, mode.id) and
die("add_failure_mode: failure mode already exists: " ~ id);
me.failure_modes[mode.id] = { mode: mode, trigger: nil };
mode.bind(proproot);
},
##
# Remove a failure mode from the system.
# id: FailureMode id string, e.g. "systems/pitot"
remove_failure_mode: func(id) {
contains(me.failure_modes, id) or
die("remove_failure_mode: failure mode does not exist: " ~ mode_id);
var trigger = me.failure_modes[id].trigger;
if (trigger != nil)
me._discard_trigger(trigger);
me.failure_modes[id].unbind();
props.globals.getNode(proproot ~ id).remove();
delete(me.failure_modes, id);
},
##
# Removes all failure modes from the system.
remove_all: func {
foreach(var id; keys(me.failure_modes))
me.remove_failure_mode(id);
},
##
# Attach a trigger to the given failure mode. Discards the current trigger
# if any.
#
# mode_id: FailureMode id string, e.g. "systems/pitot"
# trigger: Trigger object or nil.
set_trigger: func(mode_id, trigger) {
contains(me.failure_modes, mode_id) or
die("set_trigger: failure mode does not exist: " ~ mode_id);
var mode = me.failure_modes[mode_id];
if (mode.trigger != nil)
me._discard_trigger(mode.trigger);
mode.trigger = trigger;
if (trigger == nil) return;
trigger.bind(proproot ~ mode_id);
trigger.on_fire = func _failmgr.on_trigger_activated(trigger);
trigger.reset();
if (trigger.requires_polling) {
me.pollable_trigger_count += 1;
if (me.enabled() and !me.timer.isRunning)
me.timer.start();
}
trigger.enable();
},
##
# Returns the trigger object attached to the given failure mode.
# mode_id: FailureMode id string, e.g. "systems/pitot"
get_trigger: func(mode_id) {
contains(me.failure_modes, mode_id) or
die("get_trigger: failure mode does not exist: " ~ mode_id);
return me.failure_modes[mode_id].trigger;
},
##
# Observer interface. Called from asynchronous triggers when they fire.
# trigger: Reference to the calling trigger.
on_trigger_activated: func(trigger) {
var found = 0;
foreach (var id; keys(me.failure_modes)) {
if (me.failure_modes[id].trigger == trigger) {
me.failure_modes[id].mode.set_failure_level(1);
found = 1;
break;
}
}
found or die("FailureMgr.on_trigger_activated: trigger not found");
},
##
# Enable the failure manager.
_enable: func {
foreach(var id; keys(me.failure_modes)) {
var trigger = me.failure_modes[id].trigger;
trigger != nil and trigger.enable();
}
if (me.pollable_trigger_count > 0)
me.timer.start();
},
##
# Suspends failure manager activity. Pollable triggers will not be updated
# and all triggers will be disabled.
_disable: func {
me.timer.stop();
foreach(var id; keys(me.failure_modes)) {
var trigger = me.failure_modes[id].trigger;
trigger != nil and trigger.disable();
}
},
##
# Returns enabled status.
enabled: func {
getprop(proproot ~ "enabled");
},
##
# Poll loop. Updates pollable triggers and applies a failure level
# when they fire.
_update: func {
foreach (var id; keys(me.failure_modes)) {
var failure = me.failure_modes[id];
if (failure.trigger != nil and !failure.trigger.fired) {
var level = failure.trigger.update();
if (level > 0 and level != failure.mode.get_failure_level())
failure.mode.set_failure_level(level);
}
}
},
##
# Detaches a trigger from the system.
_discard_trigger: func(trigger) {
trigger.disable();
trigger.unbind();
if (trigger.requires_polling) {
me.pollable_trigger_count -= 1;
me.pollable_trigger_count == 0 and me.timer.stop();
}
},
##
# Reinit listener. Sets all failure modes to "working fine".
_on_reinit: func {
foreach (var id; keys(me.failure_modes)) {
var failure = me.failure_modes[id];
failure.mode.set_failure_level(0);
if (failure.trigger != nil) {
me._discard_trigger(failure.trigger);
failure.trigger = nil;
}
}
}
};
##
# Module initialization
var _init = func {
removelistener(lsnr);
_failmgr.init();
# Load legacy failure modes for backwards compatibility
io.load_nasal(getprop("/sim/fg-root") ~
"/Aircraft/Generic/Systems/compat_failure_modes.nas");
}
var lsnr = setlistener("/nasal/FailureMgr/loaded", _init);

229
Nasal/FailureMgr/public.nas Normal file
View file

@ -0,0 +1,229 @@
# Failure Manager public interface
#
# Copyright (C) 2014 Anton Gomez Alvedro
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
var proproot = "sim/failure-manager/";
##
# Subscribe a new failure mode to the system.
#
# id: Unique identifier for this failure mode.
# eg: "engine/carburetor-ice"
#
# description: Short text description, suitable for printing to the user.
# eg: "Ice in the carburetor"
#
# actuator: Object implementing the FailureActuator interface.
# Used by the failure manager to apply a certain level of
# failure to the failure mode.
var add_failure_mode = func(id, description, actuator) {
_failmgr.add_failure_mode(
FailureMode.new(id, description, actuator));
}
##
# Remove a failure mode from the system.
# id: FailureMode id string, e.g. "systems/pitot"
var remove_failure_mode = func(id) {
_failmgr.remove_failure_mode(id);
}
##
# Removes all failure modes from the failure manager.
var remove_all = func {
_failmgr.remove_all();
}
##
# Attaches a trigger to the given failure mode. Discards the current trigger
# if any.
#
# mode_id: FailureMode id string, e.g. "systems/pitot"
# trigger: Trigger object or nil. Nil will just detach the current trigger
var set_trigger = func(mode_id, trigger) {
_failmgr.set_trigger(mode_id, trigger);
}
##
# Returns the trigger object attached to the given failure mode.
# mode_id: FailureMode id string, e.g. "systems/pitot"
var get_trigger = func(mode_id) {
_failmgr.get_trigger(mode_id);
}
##
# Applies a certain level of failure to this failure mode.
#
# mode_id: Failure mode id string.
# level: Floating point number in the range [0, 1]
# Zero represents no failure and one means total failure.
var set_failure_level = func (mode_id, level) {
setprop(proproot ~ mode_id ~ "/failure-level", level);
}
##
# Allows applications to disable the failure manager and restore it later on.
# While disabled, no failure modes will be activated from the failure manager.
var enable = func setprop(proproot ~ "enabled", 1);
var disable = func setprop(proproot ~ "enabled", 0);
##
# Encapsulates a condition that when met, will make the failure manager to
# apply a certain level of failure to the failure mode it is bound to.
#
# Two types of triggers are supported: pollable and asynchronous.
#
# Pollable triggers require periodic check for trigger conditions. For example,
# an altitude trigger will need to poll current altitude until the fire
# condition is reached.
#
# Asynchronous trigger do not require periodic updates. They can detect
# the firing condition by themselves by using timers or listeners.
# Async triggers must call the inherited method on_fire() to let the Failure
# Manager know about the fired condition.
#
# See Aircraft/Generic/Systems/failures.nas for concrete examples of triggers.
var Trigger = {
# 1 for pollable triggers, 0 for async triggers.
requires_polling: 0,
new: func {
return {
parents: [Trigger],
params: {},
fired: 0,
##
# Async triggers shall call the on_fire() callback when their fire
# conditions are met to notify the failure manager.
on_fire: func 0,
_path: nil
};
},
##
# Enables/disables the trigger. While a trigger is disabled, any timer
# or listener that could potentially own shall be disabled.
enable: func,
disable: func,
##
# Forces a check of the firing conditions. Returns 1 if the trigger fired,
# 0 otherwise.
update: func 0,
##
# Returns a printable string describing the trigger condition.
to_str: func "undefined trigger",
##
# Modify a trigger parameter. Parameters will take effect after the next
# call to reset()
set_param: func(param, value) {
contains(me.params, param) or
die("Trigger.set_param: undefined param: " ~ param);
me._path != nil or
die("Trigger.set_param: Unbound trigger");
setprop(sprintf("%s/%s",me._path, param), value);
},
##
# Reload trigger parameters and reset internal state, i.e. start from
# scratch. If the trigger was fired, the trigger is set to not fired.
reset: func {
me._path or die("Trigger.reset: unbound trigger");
foreach (var p; keys(me.params))
me.params[p] = getprop(sprintf("%s/%s", me._path, p));
me.fired = 0;
me._path != nil and setprop(me._path ~ "/reset", 0);
},
##
# Creates an interface for the trigger in the property tree.
# Every parameter in the params hash will be exposed, in addition to
# a path/reset property for resetting the trigger from the prop tree.
bind: func(path) {
me._path == nil or
die("Trigger.bind(): attempt to bind an already bound trigger");
me._path = path;
props.globals.getNode(path) != nil or props.globals.initNode(path);
props.globals.getNode(path).setValues(me.params);
var reset_prop = path ~ "/reset";
props.globals.initNode(reset_prop, 0, "BOOL");
setlistener(reset_prop, func me.reset(), 0, 0);
},
##
# Removes this trigger's interface from the property tree.
unbind: func {
props.globals.getNode(me._path ~ "/reset").remove();
foreach (var p; keys(me.params))
props.globals.getNode(me._path ~ "/" ~ p).remove();
me._path = nil;
}
};
##
# FailureActuators encapsulate the actions required for activating the actual
# failure simulation.
#
# Traditionally this action was just manipulating a "serviceable" property
# somewhere, but the FailureActuator gives you more flexibility, allowing you
# to touch several properties at once or call other Nasal scripts, for example.
#
# See Aircraft/Generic/Systems/failure.nas and
# Aircraft/Generic/Systems/compat_failures.nas for some examples of actuators.
var FailureActuator = {
##
# Called from the failure manager to activate a certain level of failure.
# level: Target level of failure [0 to 1].
set_failure_level: func(level) 0,
##
# Returns the level of failure that is currently being simulated.
get_failure_level: func 0,
};

View file

@ -1,115 +0,0 @@
#############################################################################
#
# Simple sequenced ATC background chatter function
#
# Written by Curtis Olson
# Started 8 Jan 2006.
#
#############################################################################
#############################################################################
# Global shared variables
#############################################################################
var fg_root = nil;
var chatter = "UK";
var chatter_dir = "";
var chatter_min_interval = 20.0;
var chatter_max_interval = 40.0;
var next_interval = nil;
var chatter_index = 0;
var chatter_size = 0;
var chatter_list = 0;
#############################################################################
# Chatter is initialized only when actually enabled. See listener connected
# to /sim/sound/chatter/enabled.
#############################################################################
var chatter_init = func {
# default values
fg_root = getprop("/sim/fg-root");
chatter_dir = sprintf("%s/ATC/Chatter/%s", fg_root, chatter);
chatter_list = directory( chatter_dir );
chatter_size = size(chatter_list);
# seed the random number generator (with time) so we don't start in
# same place in the sequence each run.
srand();
chatter_index = int( chatter_size * rand() );
}
#############################################################################
# main update function to be called each frame
#############################################################################
var chatter_update = func {
if ( chatter_index >= chatter_size ) {
chatter_index = 0;
}
if ( substr(chatter_list[chatter_index],
size(chatter_list[chatter_index]) - 4) == ".wav" )
{
var vol =getprop("/sim/sound/chatter/volume");
if(vol == nil){vol = 0.5;}
tmpl = { path : chatter_dir, file : chatter_list[chatter_index] , volume : vol};
if ( getprop("/sim/sound/chatter/enabled") ) {
# go through the motions, but only schedule the message to play
# if atc-chatter is enabled.
printlog("info", "update atc chatter ", chatter_list[chatter_index] );
fgcommand("play-audio-sample", props.Node.new(tmpl) );
}
} else {
# skip non-wav file found in directory
}
chatter_index = chatter_index + 1;
nextChatter();
}
#############################################################################
# Use the nasal timer to update every 10 seconds
#############################################################################
var nextChatter = func {
if (!getprop("/sim/sound/chatter/enabled"))
{
next_interval = nil;
return;
}
# schedule next message in next min-max interval seconds so we have a bit
# of a random pacing
next_interval = chatter_min_interval
+ int(rand() * (chatter_max_interval - chatter_min_interval));
# printlog("info", "next chatter in ", next_interval, " seconds");
settimer(chatter_update, next_interval );
}
#############################################################################
# Start chatter processing. Also connected to chatter/enabled property as a
# listener.
#############################################################################
var startChatter = func {
if ( getprop("/sim/sound/chatter/enabled") ) {
if (fg_root == nil)
chatter_init();
if (next_interval == nil)
nextChatter();
}
}
# connect listener
_setlistener("/sim/sound/chatter/enabled", startChatter);
# start chatter immediately, if enable is already set.
settimer(startChatter, 0);

File diff suppressed because it is too large Load diff

View file

@ -65,5 +65,13 @@ var PropertyElement = {
{
me._node.getNode(key, 1).getBoolValue();
},
# Trigger an update of the element
#
# Elements are automatically updated once a frame, with a delay of one frame.
# If you wan't to get an element updated in the current frame you have to use
# this method.
update: func
{
me.setBool("update", 1);
}
};

View file

@ -161,6 +161,11 @@ var Element = {
return factory(parent_ghost);
},
# Get the canvas this element is placed on
getCanvas: func()
{
wrapCanvas(me._getCanvas());
},
# Check if elements represent same instance
#
# @param el Other Element or element ghost
@ -168,15 +173,6 @@ var Element = {
{
return me._node.equals(el._node_ghost);
},
# Trigger an update of the element
#
# Elements are automatically updated once a frame, with a delay of one frame.
# If you wan't to get an element updated in the current frame you have to use
# this method.
update: func()
{
me.setInt("update", 1);
},
# Hide/Show element
#
# @param visible Whether the element should be visible
@ -416,6 +412,7 @@ var Group = {
# ==============================================================================
# Class for a group element on a canvas with possibly geopgraphic positions
# which automatically get projected according to the specified projection.
# Each map consists of an arbitrary number of layers (canvas groups)
#
var Map = {
df_controller: nil,
@ -436,7 +433,7 @@ var Map = {
me.parents = subvec(me.parents,1);
me.del();
},
setController: func(controller=nil)
setController: func(controller=nil, arg...)
{
if (me.controller != nil) me.controller.del(me);
if (controller == nil)
@ -449,7 +446,7 @@ var Map = {
} else {
if (!isa(controller, Map.Controller))
die("OOP error: controller needs to inherit from Map.Controller");
me.controller = call(func controller.new(me), nil, var err=[]); # try...
me.controller = call(controller.new, [me]~arg, controller, var err=[]); # try...
if (size(err)) {
if (err[0] != "No such member: new") # ... and either catch or rethrow
die(err[0]);
@ -463,25 +460,29 @@ var Map = {
return me;
},
addLayer: func(factory, type_arg=nil, priority=nil, style=nil, options=nil)
addLayer: func(factory, type_arg=nil, priority=nil, style=nil, options=nil, visible=1)
{
if(contains(me.layers, type_arg))
printlog("warn", "addLayer() warning: overwriting existing layer:", type_arg);
# print("addLayer():", type_arg);
# Argument handling
if (type_arg != nil)
if (type_arg != nil) {
var layer = factory.new(type:type_arg, group:me, map:me, style:style, options:options, visible:visible);
var type = factory.get(type_arg);
else var type = factory;
var key = type_arg;
} else {
var layer = factory.new(group:me, map:me, style:style, options:options, visible:visible);
var type = factory;
var key = factory.type;
}
me.layers[type_arg] = layer;
me.layers[type_arg] = type.new(group:me, map:me, style:style,options:options);
if (priority == nil)
priority = type.df_priority;
if (priority != nil)
me.layers[type_arg].group.setInt("z-index", priority);
layer.group.setInt("z-index", priority);
return me;
return layer; # return new layer to caller() so that we can directly work with it, i.e. to register event handlers (panning/zooming)
},
getLayer: func(type_arg) me.layers[type_arg],
@ -490,6 +491,7 @@ var Map = {
setPos: func(lat, lon, hdg=nil, range=nil, alt=nil)
{
# TODO: also propage setPos events to layers and symbols (e.g. for offset maps)
me.set("ref-lat", lat);
me.set("ref-lon", lon);
if (hdg != nil)
@ -497,7 +499,7 @@ var Map = {
if (range != nil)
me.setRange(range);
if (alt != nil)
me.set("altitude", hdg);
me.set("altitude", alt);
},
getPos: func
{
@ -513,6 +515,10 @@ var Map = {
getAlt: func me.get("altitude"),
getRange: func me.get("range"),
getLatLon: func [me.get("ref-lat"), me.get("ref-lon")],
# N.B.: This always returns the same geo.Coord object,
# so its values can and will change at any time (call
# update() on the coord to ensure it is up-to-date,
# which basically calls this method again).
getPosCoord: func
{
var (lat, lon) = (me.get("ref-lat"),
@ -526,6 +532,8 @@ var Map = {
}
if (!contains(me, "coord")) {
me.coord = geo.Coord.new();
var m = me;
me.coord.update = func m.getPosCoord();
}
me.coord.set_latlon(lat,lon,alt or 0);
return me.coord;
@ -534,12 +542,14 @@ var Map = {
# me.controller.
update: func(predicate=nil)
{
var t = systime();
foreach (var l; keys(me.layers)) {
var layer = me.layers[l];
# Only update if the predicate allows
if (predicate == nil or predicate(layer))
call(layer.update, arg, layer);
layer.update();
}
printlog(_MP_dbg_lvl, "Took "~((systime()-t)*1000)~"ms to update map()");
return me;
},
};
@ -745,6 +755,24 @@ var Path = {
return me;
},
addSegmentGeo: func(cmd, coords...)
{
var coords = _arg2valarray(coords);
var num_coords = me.num_coords[cmd];
if( size(coords) != num_coords )
debug.warn
(
"Invalid number of arguments (expected " ~ num_coords ~ ")"
);
else
{
me.setInt("cmd[" ~ (me._last_cmd += 1) ~ "]", cmd);
for(var i = 0; i < num_coords; i += 1)
me.set("coord-geo[" ~ (me._last_coord += 1) ~ "]", coords[i]);
}
return me;
},
# Remove first segment
pop_front: func me._removeSegment(1),
# Remove last segment
@ -981,8 +1009,22 @@ var Image = {
# @param bottom Rectangle maximum y coordinate
# @param normalized Whether to use normalized ([0,1]) or image
# ([0, image_width]/[0, image_height]) coordinates
setSourceRect: func(left, top, right, bottom, normalized = 1)
setSourceRect: func
{
# Work with both positional arguments and named arguments.
# Support first argument being a vector instead of four separate ones.
if (size(arg) == 1)
arg = arg[0];
elsif (size(arg) and size(arg) < 4 and typeof(arg[0]) == 'vector')
arg = arg[0]~arg[1:];
if (!contains(caller(0)[0], "normalized")) {
if (size(arg) > 4)
var normalized = arg[4];
else var normalized = 1;
}
if (size(arg) >= 3)
var (left,top,right,bottom) = arg;
me._node.getNode("source", 1).setValues({
left: left,
top: top,
@ -1033,7 +1075,7 @@ var Canvas = {
# given every texture of the model will be replaced.
addPlacement: func(vals)
{
var placement = me.texture.addChild("placement", 0, 0);
var placement = me._node.addChild("placement", 0, 0);
placement.setValues(vals);
return placement;
},
@ -1052,31 +1094,32 @@ var Canvas = {
# Set the background color
#
# @param color Vector of 3 or 4 values in [0, 1]
setColorBackground: func () { me.texture.getNode('background', 1).setValue(_getColor(arg)); me; },
getColorBackground: func me.texture.get('background'),
setColorBackground: func me.set('background', _getColor(arg)),
getColorBackground: func me.get('background'),
# Get path of canvas to be used eg. in Image::setFile
getPath: func()
{
return "canvas://by-index/texture[" ~ me.texture.getIndex() ~ "]";
return "canvas://by-index/texture[" ~ me._node.getIndex() ~ "]";
},
# Destructor
#
# releases associated canvas and makes this object unusable
del: func
{
me.texture.remove();
me._node.remove();
me.parents = nil; # ensure all ghosts get destroyed
}
};
var wrapCanvas = func(canvas_ghost)
# @param g Canvas ghost
var wrapCanvas = func(g)
{
var m = {
parents: [PropertyElement, Canvas, canvas_ghost],
texture: props.wrapNode(canvas_ghost._node_ghost)
if( g != nil and g._impl == nil )
g._impl = {
parents: [PropertyElement, Canvas],
_node: props.wrapNode(g._node_ghost)
};
m._node = m.texture;
return m;
return g;
}
# Create a new canvas. Pass parameters as hash, eg:
@ -1090,7 +1133,7 @@ var wrapCanvas = func(canvas_ghost)
var new = func(vals)
{
var m = wrapCanvas(_newCanvasGhost());
m.texture.setValues(vals);
m._node.setValues(vals);
return m;
};
@ -1108,11 +1151,7 @@ var get = func(arg)
else
die("canvas.new: Invalid argument.");
var canvas_ghost = _getCanvasGhost(node._g);
if( canvas_ghost == nil )
return nil;
return wrapCanvas(canvas_ghost);
return wrapCanvas(_getCanvasGhost(node._g));
};
var getDesktop = func()

View file

@ -1,23 +0,0 @@
Nothing set in stone yet, we'll document things once the API becomes more stable and once it has been used in several dialogs and instruments
At the moment, this implements the notion of a "LayeredMap", a LayeredMap is a conventional Canvas Map which has support for easily managing "Layers",
which are internally mapped to Canvas Groups. Each Group's "visible" property is managed by the LayeredMap, so that layers can be easily
toggled on/off, i.e. via checkboxes.
Basically, the idea is this, we'll have a MVC (Model/View/Controller) setup, where:
- the Model is mapped to the drawable's meta information (i.e. position)
- the View is mapped to a conventional canvas group
- the Controller is mapped to a bunch of property/timer callbacks to control the Model/View
Model = PositionedSource
View = Canvas
Controller = control properties (zoom, range etc)
LayerElement = callback to create a canvas group
Layer = canvas.Group
Map -> LayeredMap -> GenericMap -> AirportMap

View file

@ -7,12 +7,14 @@ var gui = {
var gui_dir = getprop("/sim/fg-root") ~ "/Nasal/canvas/gui/";
var loadGUIFile = func(file) io.load_nasal(gui_dir ~ file, "canvas");
var loadWidget = func(name) loadGUIFile("widgets/" ~ name ~ ".nas");
var loadDialog = func(name) loadGUIFile("dialogs/" ~ name ~ ".nas");
loadGUIFile("Config.nas");
loadGUIFile("Style.nas");
loadGUIFile("Widget.nas");
loadGUIFile("styles/DefaultStyle.nas");
loadWidget("Button");
loadWidget("Label");
loadWidget("ScrollArea");
var style = DefaultStyle.new("AmbianceClassic");
@ -24,23 +26,24 @@ var WindowButton = {
_name: name
};
m._focus_policy = m.NoFocus;
m._setRoot( parent.createChild("image", "WindowButton-" ~ name) );
m._setView({_root: parent.createChild("image", "WindowButton-" ~ name)});
return m;
},
# protected:
_onStateChange: func
{
var file = style._dir_decoration ~ "/" ~ me._name;
file ~= me._window._focused ? "_focused" : "_unfocused";
var window_focus = me._windowFocus();
file ~= window_focus ? "_focused" : "_unfocused";
if( me._active )
if( me._down )
file ~= "_pressed";
else if( me._hover )
file ~= "_prelight";
else if( me._window._focused )
else if( window_focus )
file ~= "_normal";
me._root.set("src", file ~ ".png");
me._view._root.set("src", file ~ ".png");
}
};
@ -53,6 +56,7 @@ var Window = {
var ghost = _newWindowGhost(id);
var m = {
parents: [Window, PropertyElement, ghost],
_ghost: ghost,
_node: props.wrapNode(ghost._node_ghost),
_focused: 0,
_focused_widget: nil,
@ -80,7 +84,7 @@ var Window = {
if( me["_canvas"] != nil )
{
var placements = me._canvas.texture.getChildren("placement");
var placements = me._canvas._node.getChildren("placement");
# Do not remove canvas if other placements exist
if( size(placements) > 1 )
foreach(var p; placements)
@ -108,7 +112,7 @@ var Window = {
];
me._canvas = new({
size: [2 * size[0], 2 * size[1]],
size: [size[0], size[1]],
view: size,
placement: {
type: "window",
@ -124,7 +128,10 @@ var Window = {
"blend-destination-alpha": "one"
});
me._canvas._focused_widget = nil;
me._canvas.data("focused", me._focused);
me._canvas.addEventListener("mousedown", func me.raise());
return me._canvas;
},
# Set an existing canvas to be used for this Window
@ -135,26 +142,30 @@ var Window = {
canvas_.addPlacement({type: "window", "id": me.get("id")});
me['_canvas'] = canvas_;
canvas_.data("focused", me._focused);
# prevent resizing if canvas is placed from somewhere else
me.onResize = nil;
},
# Get the displayed canvas
getCanvas: func()
getCanvas: func(create = 0)
{
if( me['_canvas'] == nil and create )
me.createCanvas();
return me['_canvas'];
},
getCanvasDecoration: func()
{
return wrapCanvas(me._getCanvasDecoration());
},
addWidget: func(w)
setLayout: func(l)
{
append(me._widgets, w);
w._window = me;
if( size(me._widgets) == 2 )
w.setFocus();
w._onStateChange();
if( me['_canvas'] == nil )
me.createCanvas();
me._canvas.update(); # Ensure placement is applied
me._ghost.setLayout(l);
return me;
},
#
@ -223,6 +234,8 @@ var Window = {
# protected:
_onStateChange: func
{
var event = canvas.CustomEvent.new("wm.focus-" ~ (me._focused ? "in" : "out"));
if( me._getCanvasDecoration() != nil )
{
# Stronger shadow for focused windows
@ -233,10 +246,16 @@ var Window = {
me._title_bar_bg.set("fill", style.getColor("title" ~ suffix));
me._title.set( "fill", style.getColor("title-text" ~ suffix));
me._top_line.set( "stroke", style.getColor("title-highlight" ~ suffix));
me.getCanvasDecoration()
.data("focused", me._focused)
.dispatchEvent(event);
}
foreach(var w; me._widgets)
w._onStateChange();
if( me.getCanvas() != nil )
me.getCanvas()
.data("focused", me._focused)
.dispatchEvent(event);
},
# private:
_propCallback: func(child, mode)
@ -387,8 +406,7 @@ var Window = {
var button_close = WindowButton.new(title_bar, "close")
.move(x, y);
button_close.onClick = func me.del();
me.addWidget(button_close);
button_close.listen("clicked", func me.del());
# title
me._title = title_bar.createChild("text", "title")
@ -453,11 +471,10 @@ var initDemo = func
{
debug.dump( props.wrapNode(event.target._node_ghost) );
});
my_canvas.addEventListener("click", func
my_canvas.addEventListener("click", func(e)
{
var dlg = canvas.Window.new([400,300], "dialog")
.set("resize", 1);
var my_canvas = dlg.createCanvas()
.set("background", style.getColor("bg_color"));
@ -466,7 +483,7 @@ var initDemo = func
my_canvas.addEventListener("drag", func(e) { printf("drag: screen(%.1f|%.1f) client(%.1f|%.1f) local(%.1f|%.1f) delta(%.1f|%.1f)", e.screenX, e.screenY, e.clientX, e.clientY, e.localX, e.localY, e.deltaX, e.deltaY); });
my_canvas.addEventListener("wheel", func(e) { printf("wheel: screen(%.1f|%.1f) client(%.1f|%.1f) %.1f", e.screenX, e.screenY, e.clientX, e.clientY, e.deltaY); });
var root = my_canvas.createGroup();
root.createChild("image")
root.createChild("image")
.set("src", "http://wiki.flightgear.org/skins/common/images/icons-fg-135.png");
var text =
root.createChild("text")
@ -492,22 +509,25 @@ root.createChild("image")
text.addEventListener("mousemove", func(e) { printf("move: screen(%.1f|%.1f) client(%.1f|%.1f) local(%.1f|%.1f) delta(%.1f|%.1f)", e.screenX, e.screenY, e.clientX, e.clientY, e.localX, e.localY, e.deltaX, e.deltaY); });
text.set("fill", style.getColor("text_color"));
dlg.addWidget( gui.widgets.Button.new(root, style, {size: [64, 26]})
gui.widgets.Button.new(root, style, {})
.setText("Ok")
.move(20, 250) );
dlg.addWidget( gui.widgets.Button.new(root, style, {size: [64, 26]})
.move(20, 250)
.setSize(64, 26);
gui.widgets.Button.new(root, style, {})
.setText("Apply")
.move(100, 250) );
dlg.addWidget( gui.widgets.Button.new(root, style, {size: [64, 64]})
.move(100, 250)
.setSize(64, 26);
gui.widgets.Button.new(root, style, {})
.setText("Cancel")
.move(180, 200) );
.move(180, 200)
.setSize(64, 64);
var scroll = gui.widgets.ScrollArea.new(root, style, {size: [96, 128]})
.move(20, 100);
var scroll = gui.widgets.ScrollArea.new(root, style, {})
.move(20, 100)
.setSize(96, 128);
var txt = scroll.getContent().createChild("text")
.set("text", "01hallo\n02asdasd\n03\n04\n05asdasd06\n07ß\n08\n09asdasd\n10\n11");
scroll.update();
dlg.addWidget(scroll);
txt.addEventListener("mouseover", func txt.set("fill", "red"));
txt.addEventListener("mouseout", func txt.set("fill", "green"));

View file

@ -17,5 +17,10 @@ var Config = {
return val;
return default;
},
set: func(key, value)
{
me._cfg[key] = value;
return me;
}
};

View file

@ -8,19 +8,50 @@ gui.Widget = {
#
new: func(derived)
{
return {
var m = canvas.Widget.new({
parents: [derived, gui.Widget],
_focused: 0,
_focus_policy: gui.Widget.NoFocus,
_hover: 0,
_root: nil,
_size: [64, 64]
};
_enabled: 1,
_view: nil,
_pos: [0, 0],
_size: [32, 32]
});
m.setMinimumSize([16, 16]);
m.setSizeHint([32, 32]);
m.setMaximumSize([m._MAX_SIZE, m._MAX_SIZE]);
m.setSetGeometryFunc(m._impl.setGeometry);
return m;
},
setFixedSize: func(x, y)
{
me.setMinimumSize([x, y]);
me.setSizeHint([x, y]);
me.setMaximumSize([x, y]);
},
setEnabled: func(enabled)
{
if( me._enabled == enabled )
return me;
me._enabled = enabled;
me.clearFocus();
me._onStateChange();
return me;
},
# Move the widget to the given position (relative to its parent)
move: func(x, y)
{
me._root.setTranslation(x, y);
me._pos[0] = x;
me._pos[1] = y;
if( me._view != nil )
me._view._root.setTranslation(x, y);
return me;
},
#
@ -28,6 +59,20 @@ gui.Widget = {
{
me._size[0] = w;
me._size[1] = h;
if( me._view != nil )
me._view.setSize(me, w, h);
return me;
},
# Set geometry of widget (usually used by layouting system)
#
# @param geom [<x>, <y>, <width>, <height>]
setGeometry: func(geom)
{
me.move(geom[0], geom[1]);
me.setSize(geom[2], geom[3]);
me._onStateChange();
return me;
},
#
setFocus: func
@ -35,13 +80,17 @@ gui.Widget = {
if( me._focused )
return me;
if( me._window._focused_widget != nil )
me._window._focused_widget.clearFocus();
var canvas = me.getCanvas();
if( canvas._focused_widget != nil )
canvas._focused_widget.clearFocus();
if( !me._enabled )
return me;
me._focused = 1;
me._window._focused_widget = me;
canvas._focused_widget = me;
me.onFocusIn();
me._trigger("focus-in");
me._onStateChange();
return me;
@ -53,38 +102,61 @@ gui.Widget = {
return me;
me._focused = 0;
me._window._focused_widget = nil;
me.getCanvas()._focused_widget = nil;
me.onFocusOut();
me._trigger("focus-out");
me._onStateChange();
return me;
},
onFocusIn: func {},
onFocusOut: func {},
onMouseEnter: func {},
onMouseLeave: func {},
listen: func(type, cb)
{
me._view._root.addEventListener("cb." ~ type, cb);
return me;
},
# protected:
_MAX_SIZE: 32768, # size for "no size-limit"
_onStateChange: func {},
_setRoot: func(el)
_setView: func(view)
{
me._root = el;
el.addEventListener("mouseenter", func {
me._view = view;
var root = view._root;
var canvas = root.getCanvas();
me.setCanvas(canvas);
canvas.addEventListener("wm.focus-in", func {
me._onStateChange();
});
canvas.addEventListener("wm.focus-out", func {
me._onStateChange();
});
root.addEventListener("mouseenter", func {
me._hover = 1;
me.onMouseEnter();
me._trigger("mouse-enter");
me._onStateChange();
});
el.addEventListener("mousedown", func {
root.addEventListener("mousedown", func {
if( bits.test(me._focus_policy, me.ClickFocus / 2) )
{
me.setFocus();
me._window.setFocus();
}
});
el.addEventListener("mouseleave", func {
root.addEventListener("mouseleave", func {
me._hover = 0;
me.onMouseLeave();
me._trigger("mouse-leave");
me._onStateChange();
});
},
_trigger: func(type, data = nil)
{
me._view._root.dispatchEvent(
canvas.CustomEvent.new("cb." ~ type, {detail: data})
);
return me;
},
_windowFocus: func
{
var canvas = me.getCanvas();
return canvas != nil ? canvas.data("focused") : 0;
}
};

View file

@ -0,0 +1,124 @@
var AircraftCenter = {
show: func
{
var dlg = canvas.Window.new([550,500], "dialog")
.set("title", "Aircraft Center")
.set("resize", 1);
dlg.getCanvas(1)
.set("background", canvas.style.getColor("bg_color"));
var root = dlg.getCanvas().createGroup();
var vbox = VBoxLayout.new();
dlg.setLayout(vbox);
var packages = pkg.root.search({
'rating-FDM': 2,
'rating-cockpit': 2,
'rating-model': 2,
'rating-systems': 1
});
var info_text =
"Install/remove aircrafts (Showing " ~ size(packages) ~ " aircrafts)";
vbox.addItem(
gui.widgets.Label.new(root, style, {wordWrap: 1})
.setText(info_text)
);
var scroll = gui.widgets.ScrollArea.new(root, style, {size: [96, 128]})
.move(20, 100);
vbox.addItem(scroll, 1);
var content = scroll.getContent()
.set("font", "LiberationFonts/LiberationSans-Bold.ttf")
.set("character-size", 16)
.set("alignment", "left-center");
var list = VBoxLayout.new();
scroll.setLayout(list);
foreach(var package; packages)
{
var row = HBoxLayout.new();
list.addItem(row);
var image_label = gui.widgets.Label.new(content, style, {});
image_label.setFixedSize(171, 128);
row.addItem(image_label);
var thumbs = package.thumbnails;
if( size(thumbs) > 0 )
image_label.setImage(thumbs[0]);
else
image_label.setText("No thumbnail available");
var detail_box = VBoxLayout.new();
row.addItem(detail_box);
row.addSpacing(5);
var title_box = HBoxLayout.new();
detail_box.addItem(title_box);
title_box.addItem(
gui.widgets.Label.new(content, style, {})
.setText(package.name)
);
title_box.addStretch(1);
(func {
var p = package;
var b = gui.widgets.Button.new(content, style, {});
var installed = p.installed;
var install_text = sprintf("Install (%.1fMB)", p.fileSize/1024/1024);
if( installed )
b.setText("Remove");
else
b.setText(install_text);
b.listen("clicked", func
{
if( installed )
{
p.uninstall();
installed = 0;
b.setText(install_text);
}
else
{
b.setEnabled(0)
.setText("Wait...");
p.install()
.progress(func(i, cur, total)
b.setText(sprintf("%.1f%%", (cur / total) * 100))
)
.fail(func b.setText('Failed'))
.done(func {
installed = 1;
b.setText("Remove")
.setEnabled(1);
});
}
});
title_box.addItem(b);
})();
var description = package.description;
if( size(description) <= 0 )
{
foreach(var cat; ["FDM", "systems", "cockpit", "model"])
description ~= cat ~ ": " ~ package.lprop("rating/" ~ cat) ~ "\n";
}
detail_box.addItem(
gui.widgets.Label.new(content, style, {wordWrap: 1})
.setText(description)
);
detail_box.addStretch(1);
}
}
};
AircraftCenter.show();

View file

@ -27,32 +27,40 @@ DefaultStyle.widgets.button = {
padding: [6, 8, 6, 8],
new: func(parent, cfg)
{
me.element = parent.createChild("group", "button");
me.size = cfg.get("size", [26, 26]);
me._root = parent.createChild("group", "button");
me._bg =
me.element.rect( 3,
3,
me.size[0] - 6,
me.size[1] - 6,
{"border-radius": 5} );
me._root.createChild("path");
me._border =
me.element.createChild("image", "button")
.set("slice", "10 12") #"7")
.setSize(me.size);
me._root.createChild("image", "button")
.set("slice", "10 12"); #"7")
me._label =
me.element.createChild("text")
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
me._root.createChild("text")
.set("font", "LiberationFonts/LiberationSans-Regular.ttf")
.set("character-size", 14)
.set("alignment", "center-baseline");
},
setText: func(text)
setSize: func(model, w, h)
{
me._bg.reset()
.rect(3, 3, w - 6, h - 6, {"border-radius": 5});
me._border.setSize(w, h);
},
setText: func(model, text)
{
me._label.set("text", text);
# TODO get real font metrics
model.setMinimumSize([size(text) * 5 + 6, 16]);
model.setSizeHint([size(text) * 8 + 16, 32]);
return me;
},
update: func(active, focused, hover, backdrop)
update: func(model)
{
var backdrop = !model._windowFocus();
var (w, h) = model._size;
var file = me._style._dir_widgets ~ "/";
if( backdrop )
{
file ~= "backdrop-";
@ -62,42 +70,159 @@ DefaultStyle.widgets.button = {
me._label.set("fill", me._style.getColor("fg_color"));
file ~= "button";
if( active )
if( model._down )
{
file ~= "-active";
me._label.setTranslation(me.size[0] / 2 + 1, me.size[1] / 2 + 6);
me._label.setTranslation(w / 2 + 1, h / 2 + 6);
}
else
me._label.setTranslation(me.size[0] / 2, me.size[1] / 2 + 5);
me._label.setTranslation(w / 2, h / 2 + 5);
if( focused and !backdrop )
if( model._enabled )
{
if( model._focused and !backdrop )
file ~= "-focused";
if( hover and !active )
if( model._hover and !model._down )
{
file ~= "-hover";
me._bg.set("fill", me._style.getColor("button_bg_color_hover"));
}
else
me._bg.set("fill", me._style.getColor("button_bg_color"));
}
else
{
file ~= "-disabled";
me._bg.set("fill", me._style.getColor("button_bg_color_insensitive"));
}
me._border.set("src", file ~ ".png");
}
};
# A label
DefaultStyle.widgets.label = {
new: func(parent, cfg)
{
me._root = parent.createChild("group", "label");
},
setSize: func(model, w, h)
{
if( me['_bg'] != nil )
me._bg.reset().rect(0, 0, w, h);
if( me['_img'] != nil )
me._img.set("size[0]", w)
.set("size[1]", h);
if( me['_text'] != nil )
{
# TODO different alignment
me._text.setTranslation(2, 2 + h / 2);
me._text.set(
"max-width",
model._cfg.get("wordWrap", 0) ? (w - 4) : 0
);
}
return me;
},
setText: func(model, text)
{
if( text == nil or size(text) == 0 )
{
model.setHeightForWidthFunc(nil);
return me._deleteElement('text');
}
me._createElement("text", "text")
.set("text", text)
.set("fill", "black");
if( model._cfg.get("wordWrap", 0) )
{
var m = me;
model.setHeightForWidthFunc(func(w) m.heightForWidth(w));
}
else
{
# TODO get real font metrics
model.setMinimumSize([size(text) * 5 + 4, 14]);
model.setSizeHint([size(text) * 5 + 14, 24]);
}
return me;
},
setImage: func(model, img)
{
if( img == nil or size(img) == 0 )
return me._deleteElement('img');
me._createElement("img", "image")
.set("src", img)
.set("preserveAspectRatio", "xMidYMid slice");
return me;
},
# @param bg CSS color or 'none'
setBackground: func(model, bg)
{
if( bg == nil or bg == "none" )
return me._deleteElement("bg");
me._createElement("bg", "path")
.set("fill", bg);
me.setSize(model, model._size[0], model._size[1]);
return me;
},
heightForWidth: func(w)
{
if( me['_text'] == nil )
return -1;
return math.max(14, me._text.heightForWidth(w - 4));
},
# protected:
_createElement: func(name, type)
{
var mem = '_' ~ name;
if( me[ mem ] == nil )
{
me[ mem ] = me._root.createChild(type, "label-" ~ name);
if( type == "text" )
{
me[ mem ].set("font", "LiberationFonts/LiberationSans-Regular.ttf")
.set("character-size", 14)
.set("alignment", "left-center");
}
}
return me[ mem ];
},
_deleteElement: func(name)
{
name = '_' ~ name;
if( me[ name ] != nil )
{
me[ name ].del();
me[ name ] = nil;
}
return me;
}
};
# ScrollArea
DefaultStyle.widgets["scroll-area"] = {
new: func(parent, cfg)
{
me.element = parent.createChild("group", "scroll-area");
me._root = parent.createChild("group", "scroll-area");
me._bg = me.element.createChild("path", "background")
me._bg = me._root.createChild("path", "background")
.set("fill", "#e0e0e0");
me.content = me.element.createChild("group", "scroll-content")
me.content = me._root.createChild("group", "scroll-content")
.set("clip-frame", Element.PARENT);
me.vert = me._newScroll(me.element, "vert");
me.horiz = me._newScroll(me.element, "horiz");
me.vert = me._newScroll(me._root, "vert");
me.horiz = me._newScroll(me._root, "horiz");
},
setColorBackground: func
{
@ -105,25 +230,25 @@ DefaultStyle.widgets["scroll-area"] = {
var arg = arg[0];
me._bg.setColorFill(arg);
},
update: func(widget)
update: func(model)
{
me.horiz.reset();
if( widget._max_scroll[0] > 1 )
if( model._max_scroll[0] > 1 )
# only show scroll bar if horizontally scrollable
me.horiz.moveTo(widget._pos[0], widget._size[1] - 2)
.horiz(widget._size[0] - widget._max_scroll[0]);
me.horiz.moveTo(model._scroll_pos[0], model._size[1] - 2)
.horiz(model._size[0] - model._max_scroll[0]);
me.vert.reset();
if( widget._max_scroll[1] > 1 )
if( model._max_scroll[1] > 1 )
# only show scroll bar if vertically scrollable
me.vert.moveTo(widget._size[0] - 2, widget._pos[1])
.vert(widget._size[1] - widget._max_scroll[1]);
me.vert.moveTo(model._size[0] - 2, model._scroll_pos[1])
.vert(model._size[1] - model._max_scroll[1]);
me._bg.reset()
.rect(0, 0, widget._size[0], widget._size[1]);
.rect(0, 0, model._size[0], model._size[1]);
me.content.set(
"clip",
"rect(0, " ~ widget._size[0] ~ ", " ~ widget._size[1] ~ ", 0)"
"rect(0, " ~ model._size[0] ~ ", " ~ model._size[1] ~ ", 0)"
);
},
# private:

View file

@ -4,59 +4,71 @@ gui.widgets.Button = {
var cfg = Config.new(cfg);
var m = gui.Widget.new(gui.widgets.Button);
m._focus_policy = m.StrongFocus;
m._active = 0;
m._down = 0;
m._checkable = 0;
m._flat = cfg.get("flat", 0);
if( style != nil and !m._flat )
{
m._button = style.createWidget(parent, "button", cfg);
m._setRoot(m._button.element);
}
m._setView( style.createWidget(parent, "button", cfg) );
return m;
},
setText: func(text)
{
me._button.setText(text);
me._view.setText(me, text);
return me;
},
setActive: func
setCheckable: func(checkable)
{
if( me._active )
me._checkable = checkable;
return me;
},
setChecked: func(checked = 1)
{
if( !me._checkable or me._down == checked )
return me;
me._active = 1;
me._trigger("clicked", {checked: checked});
me._trigger("toggled", {checked: checked});
me._down = checked;
me._onStateChange();
return me;
},
clearActive: func
setDown: func(down = 1)
{
if( !me._active )
if( me._checkable or me._down == down )
return me;
me._active = 0;
me._down = down;
me._onStateChange();
return me;
},
onClick: func {},
toggle: func
{
if( !me._checkable )
me._trigger("clicked", {checked: 0});
else
me.setChecked(!me._down);
return me;
},
# protected:
_onStateChange: func
{
if( me._button != nil )
me._button.update(me._active, me._focused, me._hover, !me._window._focused);
if( me._view != nil )
me._view.update(me);
},
_setRoot: func(el)
_setView: func(view)
{
el.addEventListener("mousedown", func me.setActive());
el.addEventListener("mouseup", func me.clearActive());
var el = view._root;
el.addEventListener("mousedown", func if( me._enabled ) me.setDown(1));
el.addEventListener("mouseup", func if( me._enabled ) me.setDown(0));
el.addEventListener("click", func if( me._enabled ) me.toggle());
# Use 'call' to ensure 'me' is not set and can be used in the closure of
# custom callbacks. TODO pass 'me' as argument?
el.addEventListener("click", func call(me.onClick));
el.addEventListener("mouseleave",func me.clearActive());
el.addEventListener("mouseleave",func me.setDown(0));
el.addEventListener("drag", func(e) e.stopPropagation());
call(gui.Widget._setRoot, [el], me);
call(gui.Widget._setView, [view], me);
}
};

View file

@ -0,0 +1,26 @@
gui.widgets.Label = {
new: func(parent, style, cfg)
{
var m = gui.Widget.new(gui.widgets.Label);
m._cfg = Config.new(cfg);
m._focus_policy = m.NoFocus;
m._setView( style.createWidget(parent, "label", m._cfg) );
return m;
},
setText: func(text)
{
me._view.setText(me, text);
return me;
},
setImage: func(img)
{
me._view.setImage(me, img);
return me;
},
setBackground: func(bg)
{
me._view.setBackground(me, bg);
return me;
}
};

View file

@ -3,38 +3,28 @@ gui.widgets.ScrollArea = {
{
var cfg = Config.new(cfg);
var m = gui.Widget.new(gui.widgets.ScrollArea);
m._focus_policy = m.StrongFocus;
m._active = 0;
m._pos = [0,0];
m._size = cfg.get("size", m._size);
m._focus_policy = m.NoFocus;
m._scroll_pos = [0,0];
m._max_scroll = [0, 0];
m._content_size = [0, 0];
m._layout = nil;
if( style != nil )
{
m._scroll = style.createWidget(parent, "scroll-area", cfg);
m._setRoot(m._scroll.element);
m._setView( style.createWidget(parent, "scroll-area", cfg) );
m._scroll.vert.addEventListener("mousedown", func(e) m._dragStart(e));
m._scroll.horiz.addEventListener("mousedown", func(e) m._dragStart(e));
m._scroll.vert.addEventListener
(
"drag",
func(e) m.moveTo(m._pos[0], m._drag_offsetY + e.clientY)
);
m._scroll.horiz.addEventListener
(
"drag",
func(e) m.moveTo(m._drag_offsetX + e.clientX, m._pos[1])
);
}
m.setMinimumSize([32, 32]);
return m;
},
setLayout: func(l)
{
me._layout = l;
l.setParent(me);
return me.update();
},
getContent: func()
{
return me._scroll.content;
return me._view.content;
},
# Set the background color for the content area.
#
@ -43,7 +33,7 @@ gui.widgets.ScrollArea = {
{
if( size(arg) == 1 )
var arg = arg[0];
me._scroll.setColorBackground(arg);
me._view.setColorBackground(arg);
},
# Reset the size of the content area, e.g. on window resize.
#
@ -56,7 +46,7 @@ gui.widgets.ScrollArea = {
me._size = [x,y];
me.update();
},
# Move the scrollable area to the coordinates x,y (or as far as possible) and
# Move the scrollbars to the coordinates x,y (or as far as possible) and
# update.
#
# @param x The x coordinate (positive is right)
@ -65,15 +55,15 @@ gui.widgets.ScrollArea = {
{
var bb = me._updateBB();
me._pos[0] = math.max(0, math.min(x, me._max_scroll[0]));
me._pos[1] = math.max(0, math.min(y, me._max_scroll[1]));
me._scroll_pos[0] = math.max(0, math.min(x, me._max_scroll[0]));
me._scroll_pos[1] = math.max(0, math.min(y, me._max_scroll[1]));
me.update(bb);
},
# Move the scrollable area to the top-most position and update.
moveToTop: func()
{
me._pos[1] = 0;
me._scroll_pos[1] = 0;
me.update();
},
@ -82,14 +72,14 @@ gui.widgets.ScrollArea = {
{
var bb = me._updateBB();
me._pos[1] = me._max_scroll[1];
me._scroll_pos[1] = me._max_scroll[1];
me.update(bb);
},
# Move the scrollable area to the left-most position and update.
moveToLeft: func()
{
me._pos[0] = 0;
me._scroll_pos[0] = 0;
me.update();
},
@ -98,10 +88,32 @@ gui.widgets.ScrollArea = {
{
var bb = me._updateBB();
me._pos[0] = me._max_scroll[0];
me._scroll_pos[0] = me._max_scroll[0];
me.update(bb);
},
# Get position of scrollable content
getContentPosition: func()
{
var pos = [0, 0];
if( me._max_scroll[0] > 1 )
pos[0] = (me._scroll_pos[0] / me._max_scroll[0])
* (me._content_size[0] - me._size[0]);
if( me._max_scroll[1] > 1 )
pos[1] = (me._scroll_pos[1] / me._max_scroll[1])
* (me._content_size[1] - me._size[1]);
return pos;
},
# Move content to the given position
moveContentTo: func(x, y)
{
var scroll_x = x / (me._content_size[0] - me._size[0]) * me._max_scroll[0];
var scroll_y = y / (me._content_size[1] - me._size[1]) * me._max_scroll[1];
return me.moveTo(scroll_x, scroll_y);
},
# Update scroll bar and content area.
#
# Needs to be called when the size of the content changes.
@ -110,37 +122,92 @@ gui.widgets.ScrollArea = {
if (bb == nil) bb = me._updateBB();
if (bb == nil) return me;
var offset = [ me._content_offset[0],
me._content_offset[1] ];
if( me._max_scroll[0] > 1 )
offset[0] -= (me._pos[0] / me._max_scroll[0])
* (me._content_size[0] - me._size[0]);
if( me._max_scroll[1] > 1 )
offset[1] -= (me._pos[1] / me._max_scroll[1])
* (me._content_size[1] - me._size[1]);
var pos = me.getContentPosition();
var offset = [ me._content_offset[0] - pos[0],
me._content_offset[1] - pos[1] ];
me.getContent().setTranslation(offset);
me._scroll.update(me);
me._view.update(me);
me.getContent().update();
return me;
},
# protected:
_setRoot: func(el)
_setView: func(view)
{
el.addEventListener("wheel", func(e) me.moveTo(me._pos[0], me._pos[1] - e.deltaY));
view.vert.addEventListener("mousedown", func(e) me._dragStart(e));
view.horiz.addEventListener("mousedown", func(e) me._dragStart(e));
view._root.addEventListener("mousedown", func(e)
{
var pos = me.getContentPosition();
me._drag_offsetX = pos[0] + e.clientX;
me._drag_offsetY = pos[1] + e.clientY;
});
call(gui.Widget._setRoot, [el], me);
view.vert.addEventListener
(
"drag",
func(e)
{
if( !me._enabled )
return;
me.moveTo(me._scroll_pos[0], me._drag_offsetY + e.clientY);
e.stopPropagation();
}
);
view.horiz.addEventListener
(
"drag",
func(e)
{
if( !me._enabled )
return;
me.moveTo(me._drag_offsetX + e.clientX, me._scroll_pos[1]);
e.stopPropagation();
}
);
view._root.addEventListener
(
"drag",
func(e)
{
if( !me._enabled )
return;
me.moveContentTo( me._drag_offsetX - e.clientX,
me._drag_offsetY - e.clientY );
e.stopPropagation();
}
);
view._root.addEventListener
(
"wheel",
func(e)
{
if( !me._enabled )
return;
me.moveTo(me._scroll_pos[0], me._scroll_pos[1] - e.deltaY);
e.stopPropagation();
}
);
call(gui.Widget._setView, [view], me);
},
_dragStart: func(e)
{
me._drag_offsetX = me._pos[0] - e.clientX;
me._drag_offsetY = me._pos[1] - e.clientY;
me._drag_offsetX = me._scroll_pos[0] - e.clientX;
me._drag_offsetY = me._scroll_pos[1] - e.clientY;
e.stopPropagation();
},
_updateBB: func() {
_updateBB: func()
{
# TODO only update on content resize
if( me._layout == nil )
{
var bb = me.getContent().getTightBoundingBox();
if( bb[2] < bb[0] or bb[3] < bb[1] )
@ -148,17 +215,43 @@ gui.widgets.ScrollArea = {
var w = bb[2] - bb[0];
var h = bb[3] - bb[1];
var cur_offset = me.getContent().getTranslation();
me._content_offset = [cur_offset[0] - bb[0], cur_offset[1] - bb[1]];
}
else
{
var min_size = me._layout.minimumSize();
var max_size = me._layout.maximumSize();
var size_hint = me._layout.sizeHint();
var w = math.min(max_size[0], math.max(size_hint[0], me._size[0]));
var h = math.max(
math.min(max_size[1], math.max(size_hint[1], me._size[1])),
me._layout.heightForWidth(w)
);
me._layout.setGeometry([0, 0, w, h]);
# Layout always has the origin at (0, 0)
me._content_offset = [0, 0];
}
if( w > me._size[0] )
me._max_scroll[0] = me._size[0] * (1 - me._size[0] / w);
else me._max_scroll[0] = 0;
{
var scroller_size = math.max(12, me._size[0] * (me._size[0] / w));
me._max_scroll[0] = me._size[0] - scroller_size;
}
else
me._max_scroll[0] = 0;
if( h > me._size[1] )
me._max_scroll[1] = me._size[1] * (1 - me._size[1] / h);
else me._max_scroll[1] = 0;
{
var scroller_size = math.max(12, me._size[1] * (me._size[1] / h));
me._max_scroll[1] = me._size[1] - scroller_size;
}
else
me._max_scroll[1] = 0;
me._content_size[0] = w;
me._content_size[1] = h;
var cur_offset = me.getContent().getTranslation();
me._content_offset = [cur_offset[0] - bb[0], cur_offset[1] - bb[1]];
},
}
};

View file

@ -0,0 +1,46 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'ALT-profile';
var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [MultiSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_options: { # default configuration options
active_node: "/autopilot/route-manager/active",
vnav_node: "/autopilot/route-manager/vnav/",
types: ["tc", "td", "sc", "ed"],
}
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
map: layer.map,
listeners: [],
};
layer.searcher._equals = func(a,b) a.getName() == b.getName();
append(m.listeners, setlistener(layer.options.active_node, func m.layer.update() ));
m.addVisibilityListener();
return m;
};
var del = func() {
foreach (var l; me.listeners)
removelistener(l);
};
var searchCmd = func {
var results = [];
var symNode = props.globals.getNode(me.layer.options.vnav_node);
if (symNode != nil)
foreach (var t; me.layer.options.types) {
var n = symNode.getNode(t);
if (n != nil)
append(results, n);
}
return results;
}

View file

@ -0,0 +1,32 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'ALT-profile';
var parents = [DotSym];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
SymbolLayer.get(name).df_style = {
radius: 13,
};
var element_type = "group";
var init = func {
var name = me.model.getName();
var disptext = chr(string.toupper(name[0]))~"/"~chr(string.toupper(name[1]));
var radius = me.style.radius;
me.element.createChild("path")
.setStrokeLineWidth(5)
.moveTo(-radius, 0)
.arcLargeCW(radius, radius, 0, 2 * radius, 0)
.arcLargeCW(radius, radius, 0, -2 * radius, 0)
.setColor(0.195,0.96,0.097);
me.element.createChild("text")
.setDrawMode( canvas.Text.TEXT )
.setText(disptext)
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
.setFontSize(28)
.setTranslation(25,35)
.setColor(0.195,0.96,0.097);
}

View file

@ -5,23 +5,18 @@ var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [SingleSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_style: {},
});
# N.B.: if used, this SymbolLayer should be updated every frame
# by the Map Controller, or as often as the position is changed.
var new = func(layer) {
layer.searcher._equals = func(a,b) {
a == b;
}
return {
parents: [__self__],
map: layer.map,
_model: layer.map.getPosCoord(),
};
};
var del = func;
var searchCmd = func {
var c = me.map.getPosCoord();
return [c];
};

View file

@ -1,9 +0,0 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'APS';
var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[ name ].df_controller = __self__;
var new = func(model) ; # this controller doesn't need an instance

View file

@ -1,16 +1,19 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'APS';
var parents = [DotSym];
var parents = [SVGSymbol];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
var element_type = "group";
var svg_path = "Nasal/canvas/map/Images/boeingAirplane.svg";
var element_id = "airplane";
var init = func {
canvas.parsesvg(me.element, "Nasal/canvas/map/boeingAirplane.svg");
me.draw();
# Rotate with the main aircraft.
# Will have to be adapted if intended for use with other aircraft
# (but one could simply copy the layer for that).
var draw = func {
var rot = getprop("/orientation/heading-deg");
rot -= me.layer.map.getHdg();
me.element.setRotation(rot*D2R);
};
var draw = func me.element.setRotation(me.layer.map.getHdg());

View file

@ -5,9 +5,18 @@ var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [MultiSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_style: {
line_width: 3,
scale_factor: 1,
debug: 1,
color_default: [0,0.6,0.85],
label_font_color:[0,0.6,0.85],
label_font_size: 28,
},
});
var a_instance = nil;
var new = func(layer) {
@ -17,7 +26,8 @@ var new = func(layer) {
map: layer.map,
listeners: [],
};
__self__.a_instance = m;
m.addVisibilityListener();
return m;
};
var del = func() {

View file

@ -1,13 +0,0 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'APT';
var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[ name ].df_controller = __self__;
var new = func(model) ; # this controller doesn't need an instance
var LayerController = SymbolLayer.Controller.registry[ name ];
var isActive = func(model) LayerController.a_instance.isActive(model);
var query_range = func()
die( name~".scontroller.query_range /MUST/ be provided by implementation" );

View file

@ -6,47 +6,28 @@ var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
var element_type = "group"; # we want a group, becomes "me.element"
var icon_fix = nil;
var text_fix = nil;
var icon_apt = nil;
var text_apt = nil;
# add the draw routine from airports-nd.draw here
var draw = func {
if (me.icon_fix != nil) return;
var init = func {
var icon_apt = me.element.createChild("path", name ~ " icon" )
.moveTo(-17,0)
.arcSmallCW(17,17,0,34,0)
.arcSmallCW(17,17,0,-34,0)
.close()
.setColor(0,0.6,0.85)
.setStrokeLineWidth(3);
.setColor(me.layer.style.color_default)
.setStrokeLineWidth(me.layer.style.line_width);
var text_apt = me.element.createChild("text", name ~ " label")
.setDrawMode( canvas.Text.TEXT )
.setTranslation(17,35)
.setText(me.model.id)
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
.setColor(0,0.6,0.85)
.setFontSize(28);
#me.element.setGeoPosition(lat, lon)
# .set("z-index",1); # FIXME: this needs to be configurable!!
.setColor(me.layer.style.label_font_color)
.setFontSize(me.layer.style.label_font_size);
# disabled:
if(0) {
# the fix symbol
me.icon_fix = me.element.createChild("path")
.moveTo(-15,15)
.lineTo(0,-15)
.lineTo(15,15)
.close()
.setStrokeLineWidth(3)
.setColor(0,0.6,0.85)
.setScale(0.5,0.5); # FIXME: do proper LOD handling here - we need to scale according to current texture dimensions vs. original/design dimensions
# the fix label
me.text_fix = me.element.createChild("text")
.setDrawMode( canvas.Text.TEXT )
.setText(me.model.id)
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
.setFontSize(28)
.setTranslation(5,25);
}
# FIXME: this applies scale to the whole group, better do this separately for each element?
me.element.setScale(me.layer.style.scale_factor);
};
var draw = func;

View file

@ -5,7 +5,7 @@ var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [NavaidSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_style: {
@ -25,33 +25,16 @@ var new = func(layer) {
listeners: [],
query_type:'dme',
};
##
# default styling parameters - can be overridden via addLayer( style:{key:value, ...} )
if (contains(m.layer,'style')) return m; # we already have a proper style
# otherwise, set up a default style:
m.layer.style={};
m.layer.style.debug = 0; # HACK: setting this enables benchmarking and printlog statements
m.layer.style.animation_test = 0;
# these are used by the draw() routines, see DME.symbol
m.layer.style.scale_factor = 1.0 ; # applied to the whole group for now
m.layer.style.line_width = 3.0;
m.layer.style.color_tuned = [0,1,0];
m.layer.style.color_default = [0,0.6,0.85];
m.addVisibilityListener();
return m;
};
## TODO: also move this to generator helper
var del = func() {
foreach (var l; me.listeners)
removelistener(l);
};
var searchCmd = func {
printlog(_MP_dbg_lvl, "Running query:", me.query_type);
var range = me.map.getRange();
if (range == nil) return;
return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type);
};
var searchCmd = NavaidSymbolLayer.make(query:'dme');

View file

@ -1,9 +0,0 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'DME';
var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[ name ].df_controller = __self__;
var new = func(model, symbol) ; # this controller doesn't need an instance

View file

@ -49,11 +49,6 @@ var del = func {
# var DMEIcon = StyleableElement.new( [{color:IconColor}, ] );
###
# helper to tell if the symbol is already initialized or not
# TODO: encapsulate API-wise (this is a very common thing to do...)
var is_initialized = func me.icon_dme != nil;
###
# FIXME: these should probably be part of MapStructure itself
# TODO: Need to come up with a StyleableElement class for this sort of stuff
@ -64,7 +59,7 @@ var apply_styling = func {
var current_color = me.icon_dme.getColor();
var required_color = nil;
if (typeof(me.layer.map.controller["is_tuned"]) == 'func' and me.layer.map.controller.is_tuned(me.model.frequency/100))
if (typeof(me.map.controller["is_tuned"]) == 'func' and me.map.controller.is_tuned(me.model.frequency/100))
#TODO: once we support caching/instancing, we cannot just change the symbol like this - we need to use a different symbol from the cache instead
# which is why these things need to be done ONCE during init to set up a cache entry for each symbol variation to come up with a corresponding raster image
# TODO: API-wise it would make sense to maintain a vector of required keys, so that the style hash can be validated in the ctor of the layer
@ -90,7 +85,6 @@ var apply_styling = func {
# TODO: also needs to be aware of current range, so that proper LOD handling can be implemented
var apply_scale = func {
# add all symbols here that need scaling
# print("Scaling:", me.layer.style.scale_factor);
me.icon_dme.setScale( me.layer.style.scale_factor );
}
@ -106,11 +100,8 @@ var init_cache = func {
##
# init is done separately to prepare support for caching (instanced symbols)
# NOTE: People should not be "hard-coding" things like color/size here
# these need to be encapsulated via a Hash lookup, so that things can be
# easily customized
#
var init_symbols = func {
var init = func {
# debug.dump(me.layer.style);
me.icon_dme = me.element.createChild("path")
.moveTo(-15,0)
@ -127,31 +118,21 @@ var init_symbols = func {
.horiz(-14.5)
.vert(-14.5)
.close()
.setStrokeLineWidth( me.layer.style.line_width ); #TODO: this should be style-able
.setStrokeLineWidth( me.layer.style.line_width );
# finally scale the symbol as requested, this is done last so that people can override this when creating the layer
me.apply_scale();
if (me.layer.style.animation_test) {
me.timer = maketimer(0.33, func me.animate() );
me.timer.start();
}
}
var updateRun = func {
# check if the current station is tuned or not - and style the symbol accordingly (color)
me.apply_styling();
me.draw();
}
##
# this method is REQUIRED (basically, the entry point for drawing - most others are just helpers)
# this method is REQUIRED
# check if the current station is tuned or not - and style the symbol accordingly (color)
var draw = func {
# print("DME:draw()");
# Init: will set up the symbol if it isn't already
if ( !me.is_initialized() )
me.init_symbols();
# wrapper for custom styling, based on tuned/default colors (see lookup hash above)
me.updateRun();
me.apply_styling();
};

View file

@ -5,17 +5,9 @@ var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [NavaidSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_style: {
line_width: 3,
scale_factor: 0.5,
debug: 1,
color_default: [0, 0.6, 0.85],
}
});
var new = func(layer) {
var m = {
@ -25,6 +17,8 @@ var new = func(layer) {
listeners: [],
query_type:'fix',
};
m.addVisibilityListener();
return m;
};
var del = func() {
@ -33,10 +27,5 @@ var del = func() {
removelistener(l);
};
var searchCmd = func {
printlog(_MP_dbg_lvl, "Running query:", me.query_type);
var range = me.map.getRange();
if (range == nil) return;
return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type);
};
var searchCmd = NavaidSymbolLayer.make('fix');

View file

@ -1,9 +0,0 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'FIX';
var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[ name ].df_controller = __self__;
var new = func(model) ; # this controller doesn't need an instance

View file

@ -5,45 +5,45 @@ var parents = [DotSym];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
SymbolLayer.get(name).df_style = {
line_width: 3,
scale_factor: 1,
font: "LiberationFonts/LiberationSans-Regular.ttf",
font_color: [0,0,0],
font_size: 28,
color: [0, 0.6, 0.85],
show_labels: 1,
};
var element_type = "group"; # we want a group, becomes "me.element"
var icon_fix = nil;
var text_fix = nil;
##
# used during initialization to populate the symbol cache with a FIX symbol
#
var drawFIX = func(color, width) func(group) {
var symbol = group.createChild("path")
var drawFIX = func(group) {
group.createChild("path")
.moveTo(-15,15)
.lineTo(0,-15)
.lineTo(15,15)
.close()
.setStrokeLineWidth(width)
.setColor(color)
.setScale(0.5,0.5); # FIXME: do proper LOD handling here - we need to scale according to current texture dimensions vs. original/design dimensions
return symbol;
.setStrokeLineWidth(line_width)
.setColor(color);
}
var icon_fix_cached = [
SymbolCache32x32.add(
name: "FIX",
callback: drawFIX( color:[0, 0.6, 0.85], width:3 ), # TODO: use the style hash to encapsulate styling stuff
draw_mode: SymbolCache.DRAW_CENTERED
)
];
var cache = StyleableCacheable.new(
name:name, draw_func: drawFIX,
cache: SymbolCache32x32,
draw_mode: SymbolCache.DRAW_CENTERED,
relevant_keys: ["line_width", "color"],
);
var draw = func {
if (me.icon_fix != nil) return; # fix icon already initialized
# initialize the fix symbol
me.icon_fix = icon_fix_cached[0].render(me.element);
var init = func {
# initialize the cached fix symbol
cache.render(me.element, me.style).setScale(me.style.scale_factor);
# non-cached stuff:
# FIXME: labels need to be LOD-aware (i.e. aware of MapController range, so that we can hide/show them)
me.text_fix = me.element.createChild("text")
.setDrawMode( canvas.Text.TEXT )
.setText(me.model.id)
.setFont("LiberationFonts/LiberationSans-Regular.ttf") # TODO: encapsulate styling stuff
.setFontSize(28) # TODO: encapsulate styling stuff
.setTranslation(5,25);
if (me.style.show_labels)
me.text_fix = me.newText(me.model.id).setScale(me.style.scale_factor);
}
var draw = func;

View file

@ -0,0 +1,58 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'FLT';
var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [MultiSymbolLayer],
append: func(idx, item, model) {
while (size(me.list) <= idx) {
append(me.list, nil);
} if (me.list[idx] == nil) {
me.list[idx] = me.add_sym(model);
}
append(model, item);
var pos = me.controller.getpos(item);
var cmd = me.list[idx].element._last_cmd == -1 ? canvas.Path.VG_MOVE_TO : canvas.Path.VG_LINE_TO;
me.list[idx].element.addSegmentGeo(cmd, ["N"~pos[0],"E"~pos[1]]);
#props.dump(me.list[idx].element._node);
},
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
add_sym: func(model) {
return Symbol.new(me.type, me.group, me, model);
},
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
listeners: [],
models: [],
active_path: 0,
};
layer.searcher._equals = func(a,b) a == b;
m.addVisibilityListener();
return m;
};
var del = func() {
foreach (var l; me.listeners)
removelistener(l);
};
var searchCmd = func() {
printlog(_MP_dbg_lvl, "Running query: FLT");
var hist = aircraft.history();
var path = hist.pathForHistory(1000);
printlog(_MP_dbg_lvl, "FLT size: "~size(path));
while (size(me.models) <= me.active_path) append(me.models, []);
for (var i=size(me.models[me.active_path]); i<size(path); i+=1)
me.layer.append(me.active_path, path[i], me.models[me.active_path]);
# TODO: filter by in_range()?
#debug.dump(me.models);
return me.models;
};
var getpos = Symbol.Controller.getpos;

View file

@ -0,0 +1,19 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'FLT';
var parents = [LineSymbol];
var __self__ = caller(0)[0];
LineSymbol.makeinstance( name, __self__ );
SymbolLayer.get(name).df_style = { # style to use by default
line_width: 5,
color: [0,0,1]
};
var init = func {
me.element.setColor(me.layer.style.color)
.setStrokeLineWidth(me.layer.style.line_width);
me.needs_update = 0;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 508 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 474 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 474 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 474 KiB

View file

@ -58,7 +58,7 @@
transform="translate(-364.652,-344.745)">
<g
id="airplane"
transform="translate(364.652,346.745)"
transform="translate(320.73272,291.98516)"
inkscape:label="#g3781">
<path
id="path3783"

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 MiB

After

Width:  |  Height:  |  Size: 5.8 MiB

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -5,9 +5,10 @@ var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [NavaidSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_style: {},
});
var new = func(layer) {
var m = {
@ -17,6 +18,8 @@ var new = func(layer) {
listeners: [],
query_type:'ndb',
};
m.addVisibilityListener();
return m;
};
var del = func() {
@ -24,10 +27,6 @@ var del = func() {
removelistener(l);
};
var searchCmd = func {
printlog(_MP_dbg_lvl, "Running query:", me.query_type);
var range = me.map.getRange();
if (range == nil) return;
return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type);
};
var searchCmd = NavaidSymbolLayer.make('ndb');

View file

@ -1,10 +0,0 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'NDB';
var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[ name ].df_controller = __self__;
var new = func(model) ; # this controller doesn't need an instance
var LayerController = SymbolLayer.Controller.registry[ name ];

View file

@ -1,19 +1,7 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'NDB';
var parents = [DotSym];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
var element_type = "group"; # we want a group, becomes "me.element", which we parse a SVG onto
var svg_path = "/gui/dialogs/images/ndb_symbol.svg"; # speaking of path, this is our path to use
var local_svg_path = nil; # track changes in the SVG's path
var draw = func {
if (me.svg_path == me.local_svg_path) return;
me.element.removeAllChildren();
me.local_svg_path = me.svg_path;
canvas.parsesvg(me.element, me.svg_path);
me.inited = 1;
};
DotSym.makeinstance('NDB', {
parents: [SVGSymbol],
svg_path: "/gui/dialogs/images/ndb_symbol.svg",
#cacheable: 1,
});

View file

@ -0,0 +1,54 @@
# See: http://wiki.flightgear.org/MapStructure
# TODO: this layer doesn't make sense to support for AI/MP traffic, because we don't currently have access to flightplan/routing info
# that also applies to other layers like WPT or even navaid layers that handle station tuning based on local radio settings
#
# Class things:
var name = 'RTE';
var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [MultiSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_options: { # default configuration options
active_node: "/autopilot/route-manager/active",
current_wp_node: "/autopilot/route-manager/current-wp",
}
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
map: layer.map,
listeners: [],
};
layer.searcher._equals = func(l,r) 0; # TODO: create model objects instead?
append(m.listeners, setlistener(layer.options.active_node, func m.layer.update() ));
m.addVisibilityListener();
return m;
};
var del = func() {
foreach (var l; me.listeners)
removelistener(l);
};
var searchCmd = func {
# FIXME: do we return the current route even if it isn't active?
printlog(_MP_dbg_lvl, "Running query: ", name);
var plans = []; # TODO: multiple flightplans?
# http://wiki.flightgear.org/Nasal_Flightplan
var fp = flightplan();
var fpSize = fp.getPlanSize();
if (!getprop(me.layer.options.active_node)) fpSize = 0;
var coords = [];
for (var i=0; i<fpSize; i += 1) {
var leg = fp.getWP(i);
coords ~= leg.path();
}
append(plans, coords);
return plans;
};

View file

@ -0,0 +1,18 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'RTE';
var parents = [LineSymbol];
var __self__ = caller(0)[0];
LineSymbol.makeinstance( name, __self__ );
SymbolLayer.get(name).df_style = { # style to use by default
line_width: 5,
color: [1,0,1]
};
var init = func {
me.element.setColor(me.style.color)
.setStrokeLineWidth(me.style.line_width);
};

View file

@ -1,14 +1,16 @@
# See: http://wiki.flightgear.org/MapStructure
# FIXME: this needs to be configurable via the ctor to optionally differentiate between MP and AI traffic, and exclude ground/sea traffic
# or at least use a different, non-TCAS symbol for those (looking pretty weird ATM)
#
# Class things:
var name = 'TFC';
var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [MultiSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_style: { debug: 0 }, # style to use by default
});
var model_root = props.globals.getNode("/ai/models/");
@ -22,6 +24,8 @@ var new = func(layer) {
searchCmd: searchCmd_default,
};
layer.searcher._equals = func(l,r) l.equals(r);
m.addVisibilityListener();
return m;
};
var del = func() {
@ -40,6 +44,7 @@ var TrafficModel = {
id: id,
node: node,
pos: node.getNode("position",1),
type: node.getName(),
};
return m;
},
@ -57,6 +62,13 @@ var TrafficModel = {
get_alt: func() (me.getValue("position/altitude-ft") or 0),
};
var get_alt_diff = func(model) {
# debug.dump( keys(me) );
var model_alt = model.get_alt();
var alt = me.map.getAlt();
if (alt == nil or model_alt == nil) return 0;
return alt-model_alt;
};
##
# dummy/placeholder (will be overridden in ctor and set to the default callback)
@ -66,6 +78,8 @@ var searchCmd_default = func {
# TODO: this would be a good candidate for splitting across frames
printlog(_MP_dbg_lvl, "Doing query: "~name);
if (me.map.getRange() == nil) return;
var result = [];
var models = 0;

View file

@ -1,16 +0,0 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'TFC';
var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[name].df_controller = __self__;
var new = func(model, symbol) ; # this controller doesn't need an instance
var get_alt_diff = func(model) {
# debug.dump( keys(me) );
var model_alt = model.get_alt();
var alt = getprop("/position/altitude-ft"); # FIXME: hardcoded - right, we should probably generalize the "NDSourceDriver logic found in navdisplay.mfd and make it part of MapStructure
if (alt == nil or model_alt == nil) return 0;
return alt-model_alt;
};

View file

@ -5,6 +5,17 @@ var parents = [DotSym];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
SymbolLayer.get(name).df_style = { # style to use by default
line_width: 3,
scale_factor: 1,
color_by_lvl: {
3: [1,0,0], # resolution advisory
2: [1,0.5,0], # traffic advisory
1: [1,1,1], # proximate traffic
},
color_default: [1,1,1]
};
var element_type = "group"; # we want a group, becomes "me.element"
var text_tcas = nil;
var icon_tcas = nil;
@ -12,8 +23,12 @@ var arrow_tcas = [nil,nil];
var arrow_type = nil;
var draw_tcas_arrow = nil;
var color = nil;
var threatLvl = 0e-0; # NaN to update even when threatLvl == nil
# TODO: how to integrate both styling and caching?
var draw = func {
# TODO: get rid of draw_tcas_arrow hacks
if (draw_tcas_arrow == nil)
draw_tcas_arrow = [
draw_tcas_arrow_above_500,
@ -23,7 +38,7 @@ var draw = func {
# print("Drawing traffic for:", callsign );
var threatLvl = me.model.get_threat_lvl();
var vspeed = me.model.get_vspd();
var altDiff = me.controller.get_alt_diff(me.model);
var altDiff = me.layer.controller.get_alt_diff(me.model);
# Init
if (me.text_tcas == nil) {
me.text_tcas = me.element.createChild("text")
@ -49,48 +64,54 @@ var draw = func {
me.arrow_tcas[arrow_type] = me.draw_tcas_arrow[arrow_type](me.element);
me.arrow_tcas[arrow_type].show();
}
## TODO: threat level symbols should also be moved to *.draw files
if (threatLvl != me.threatLvl) {
me.threatLvl = threatLvl;
## TODO: should threat level symbols also be moved to *.draw files?
if (threatLvl == 3) {
# resolution advisory
me.icon_tcas.moveTo(-17,-17)
.horiz(34)
.vert(34)
.horiz(-34)
.close()
.setColor(1,0,0)
.setColorFill(1,0,0);
me.text_tcas.setColor(1,0,0);
me.arrow_tcas[me.arrow_type].setColor(1,0,0);
.close();
} elsif (threatLvl == 2) {
# traffic advisory
me.icon_tcas.moveTo(-17,0)
.arcSmallCW(17,17,0,34,0)
.arcSmallCW(17,17,0,-34,0)
.setColor(1,0.5,0)
.setColorFill(1,0.5,0);
me.text_tcas.setColor(1,0.5,0);
me.arrow_tcas[me.arrow_type].setColor(1,0.5,0);
.arcSmallCW(17,17,0,-34,0);
} elsif (threatLvl == 1) {
# proximate traffic
me.icon_tcas.moveTo(-10,0)
.lineTo(0,-17)
.lineTo(10,0)
.lineTo(0,17)
.close()
.setColor(1,1,1)
.setColorFill(1,1,1);
me.text_tcas.setColor(1,1,1);
me.arrow_tcas[me.arrow_type].setColor(1,1,1);
.close();
} else {
# other traffic
me.icon_tcas.moveTo(-10,0)
.lineTo(0,-17)
.lineTo(10,0)
.lineTo(0,17)
.close()
.setColor(1,1,1);
me.text_tcas.setColor(1,1,1);
me.arrow_tcas[me.arrow_type].setColor(1,1,1);
.close();
}
}
var color = nil;
if (threatLvl != nil)
if ((var c = me.style.color_by_lvl[threatLvl]) != nil)
var color = canvas._getColor(c);
if (color == nil)
color = canvas._getColor(me.style.color_default);
if (me.color != color) {
me.color = color;
me.icon_tcas.setColor(color);
me.text_tcas.setColor(color);
me.arrow_tcas[me.arrow_type].setColor(color);
if (num(threatLvl) == nil or (threatLvl < 1 or threatLvl > 3)) {
color = [0,0,0,0];
}
me.icon_tcas.setColorFill(color);
}
if (me.style.scale_factor != me.element.getScale())
me.element.setScale(me.style.scale_factor);
};

View file

@ -5,21 +5,11 @@ var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [NavaidSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
});
var new = func(layer) {
if(0) {
# TODO: generalize and move to MapStructure.nas
var required_controllers = [ {name: 'query_range',type:'func'}, ];
foreach(var c; required_controllers) {
if (!contains(layer.map.controller, c.name) or typeof(layer.map.controller[c.name]) !=c.type)
die("Required controller is missing/invalid:"~ c.name ~ ' ['~c.type~']' );
}
}
var m = {
parents: [__self__],
layer: layer,
@ -38,6 +28,8 @@ if(0) {
}
#call(debug.dump, keys(layer));
m.changed_freq(update:0);
m.addVisibilityListener();
return m;
};
var del = func() {
@ -59,10 +51,6 @@ var changed_freq = func(update=1) {
me.active_vors[ navN.getIndex() ] = navN.getValue("frequencies/selected-mhz");
if (update) me.layer.update();
};
var searchCmd = func {
printlog(_MP_dbg_lvl, "Running query:", me.query_type);
var range = me.map.getRange();
if (range == nil) return;
return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type);
};
var searchCmd = NavaidSymbolLayer.make('vor');

View file

@ -1,9 +0,0 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'VOR';
var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[name].df_controller = __self__;
var new = func(model) ; # this controller doesn't need an instance

View file

@ -5,33 +5,31 @@ var parents = [DotSym];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
SymbolLayer.get(name).df_style = { # style to use by default
line_width: 3,
range_line_width: 3,
radial_line_width: 3,
range_dash_array: [5, 15, 5, 15, 5],
radial_dash_array: [15, 5, 15, 5, 15],
scale_factor: 1,
active_color: [0, 1, 0],
inactive_color: [0, 0.6, 0.85],
};
var element_type = "group"; # we want a group, becomes "me.element"
var icon_vor = nil; # a handle to the cached icon
var range_vor = nil; # two elements that get drawn when needed
var radial_vor = nil; # if one is nil, the other has to be nil
var active = -1;
###
# this function returns a new function that renders the symbol
# into a canvas group
# the signature of the first func should contain all customizable
# parameters (such as color/width etc)
#
# you will typically have one such function for each cacheable symbol
# and only call it once during initialization (once for each cached style/variation)
# the signature of the 2nd func remains untouched, it's internally used
##
# Callback for drawing a new VOR based on styling
#
# NOTE: callbacks that create symbols for caching MUST render them using a
# transparent background !!
#
var drawVOR = func(color, width=3) return func(group) {
# init_calls += 1; # TODO: doing this here is too fragile, this should be done by MapStructure ...
# if( init_calls >= 2)
# print("Warning: draw() routine for a cached element has been called more than once, should only happen during reset/reinit");
# print("drawing vor");
var symbol = group.createChild("path")
var drawVOR = func(group) {
group.createChild("path")
.moveTo(-15,0)
.lineTo(-7.5,12.5)
.lineTo(7.5,12.5)
@ -39,35 +37,16 @@ var drawVOR = func(color, width=3) return func(group) {
.lineTo(7.5,-12.5)
.lineTo(-7.5,-12.5)
.close()
.setStrokeLineWidth(width)
.setColor( color );
return symbol; # make this explicit, not everybody knows Nasal internals inside/out ...
.setStrokeLineWidth(line_width)
.setColor(color);
};
##
# a vector with pre-created symbols that are cached during initialization
# this needs to be done for each variation of each symbol, i.e. color/width etc
# note that scaling should not be done here, likewise for positioning via setGeoPosition()
#
# FIXME: We still need to encode styling information as part of the key/name lookup
# so that the cache entry's name contains styling info, or different maps/layers may
# overwrite their symbols
#
# icon_vor_cache[0] - inactive symbol
# icon_vor_cache[1] - active symbol
var icon_vor_cached = [
SymbolCache32x32.add(
name: "VOR-INACTIVE",
callback: drawVOR( color:[0, 0.6, 0.85], width:3 ),
draw_mode: SymbolCache.DRAW_CENTERED
),
SymbolCache32x32.add(
name: "VOR-ACTIVE",
callback: drawVOR( color:[0, 1, 0], width:3 ),
draw_mode: SymbolCache.DRAW_CENTERED
),
];
var cache = StyleableCacheable.new(
name:name, draw_func: drawVOR,
cache: SymbolCache32x32,
draw_mode: SymbolCache.DRAW_CENTERED,
relevant_keys: ["line_width", "color"],
);
##
# TODO: make this a part of the framework, for use by other layers/symbols etc
@ -84,16 +63,14 @@ var controller_check = func(layer, controller, arg...) {
}
var draw = func {
# Init
if (0 and me.model.id == "SAC") # hack to test isActive() around KSMF
setprop("instrumentation/nav[0]/frequencies/selected-mhz", me.model.frequency/100);
var active = controller_check(me.layer, 'isActive', me.model);
#print(me.model.id,"/", me.model.frequency/100, " isActive:", active);
if (active != me.active) {
#print("VOR.symbol: active != me.active: del/render event triggered");
if (me.icon_vor != nil) me.icon_vor.del();
# look up the correct symbol from the cache and render it into the group as a raster image
me.icon_vor = icon_vor_cached[active or 0].render(me.element);
#me.icon_vor = icon_vor_cached[active or 0].render(me.element);
me.style.color = active ? me.style.active_color : me.style.inactive_color;
me.icon_vor = cache.render(me.element, me.style).setScale(me.style.scale_factor);
me.active = active; # update me.active flag
}
# Update (also handles non-cached stuff, such as text labels or animations)
@ -101,24 +78,24 @@ var draw = func {
if (active) {
if (me.range_vor == nil) {
# initialize me.range and me.radial_vor
var rangeNm = me.layer.map.getRange();
var rangeNm = me.map.getRange();
# print("VOR is tuned:", me.model.id);
var radius = (me.model.range_nm/rangeNm)*345;
me.range_vor = me.element.createChild("path")
.moveTo(-radius,0)
.arcSmallCW(radius,radius,0,2*radius,0)
.arcSmallCW(radius,radius,0,-2*radius,0)
.setStrokeLineWidth(3)
.setStrokeDashArray([5, 15, 5, 15, 5])
.setColor(0,1,0);
.setStrokeLineWidth(me.style.range_line_width)
.setStrokeDashArray(me.style.range_dash_array)
.setColor(me.style.active_color);
var course = me.layer.map.controller.get_tuned_course(me.model.frequency/100);
var course = me.map.controller.get_tuned_course(me.model.frequency/100);
me.radial_vor = me.element.createChild("path")
.moveTo(0,-radius)
.vert(2*radius)
.setStrokeLineWidth(3)
.setStrokeDashArray([15, 5, 15, 5, 15])
.setColor(0,1,0)
.setStrokeLineWidth(me.style.radial_line_width)
.setStrokeDashArray(me.style.radial_dash_array)
.setColor(me.style.active_color)
.setRotation(course*D2R);
}
me.range_vor.show();

View file

@ -5,17 +5,26 @@ var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [SymbolLayer],
parents: [MultiSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
df_options: { # default configuration options
active_node: "/autopilot/route-manager/active",
current_wp_node: "/autopilot/route-manager/current-wp",
}
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
options: layer.options,
map: layer.map,
listeners: [],
};
layer.searcher._equals = func(l,r) l.equals(r);
append(m.listeners, setlistener(layer.options.active_node, func m.layer.update() ));
m.addVisibilityListener();
return m;
};
var del = func() {
@ -24,15 +33,37 @@ var del = func() {
removelistener(l);
};
var WPT_model = {
new: func(fp, idx, offset=0) {
var m = { parents:[WPT_model], idx:(idx+offset) };
var wp = fp.getWP(idx);
m.name = wp.wp_name;
var alt = wp.alt_cstr;
if (alt != 0)
m.name ~= "\n"~alt;
if (idx) var n = wp.path()[-1];
else var n = fp.getWP(1).path()[0];
(m.lat,m.lon) = (n.lat,n.lon);
return m;
},
equals: func(other) {
# this is set on symbol init, so use this for equality...
me.name == other.name
},
};
var searchCmd = func {
printlog(_MP_dbg_lvl, "Running query: "~name);
if (!getprop(me.options.active_node)) return [];
var fp = flightplan();
var fpSize = fp.getPlanSize();
var result = [];
for (var i = 1; i <fpSize; i+=1)
append(result, fp.getWP(i).path()[0] );
for (var i = 0; i < fpSize; i+=1)
append(result, WPT_model.new(fp, i));
return result;
};

View file

@ -5,9 +5,26 @@ var parents = [Symbol.Controller];
var __self__ = caller(0)[0];
Symbol.Controller.add(name, __self__);
Symbol.registry[ name ].df_controller = __self__;
var new = func(model) ; # this controller doesn't need an instance
var LayerController = SymbolLayer.Controller.registry[ name ];
var isActive = func(model) LayerController.a_instance.isActive(model);
var query_range = func()
die( name~".scontroller.query_range /MUST/ be provided by implementation" );
var last_idx = nil;
var new = func(symbol, model) {
var m = {
parents:[__self__],
listeners: [],
symbol: symbol,
model: model,
};
append(m.listeners, setlistener(symbol.layer.options.current_wp_node, func(n) {
var n = n.getValue();
if (m.last_idx == model.idx or n == model.idx)
symbol.update();
m.last_idx = n;
}, 0, 0));
m.last_idx = getprop(symbol.layer.options.current_wp_node);
return m;
}
var del = func() {
foreach (var l; me.listeners)
removelistener(l);
}

View file

@ -5,14 +5,22 @@ var parents = [DotSym];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
var element_type = "group"; # we want a group, becomes "me.element"
var base = nil;
var text_wps = nil;
SymbolLayer.get(name).df_style = { # style to use by default
line_width: 3,
scale_factor: 1,
font: "LiberationFonts/LiberationSans-Regular.ttf",
font_size: 28,
font_color: [1,0,1],
active_color: [1,0,1],
inactive_color: [1,1,1],
};
var draw = func {
if (me.base != nil) return;
me.base = me.element.createChild("path")
.setStrokeLineWidth(3)
var element_type = "group"; # we want a group, becomes "me.element"
var active = nil;
var init = func {
me.path = me.element.createChild("path")
.setStrokeLineWidth(me.style.line_width)
.moveTo(0,-25)
.lineTo(-5,-5)
.lineTo(-25,0)
@ -22,14 +30,23 @@ var draw = func {
.lineTo(25,0)
.lineTo(5,-5)
.setColor(1,1,1)
.close();
.close()
.setScale(me.style.scale_factor);
me.text_wps = wpt_grp.createChild("text")
.setDrawMode( canvas.Text.TEXT )
.setText(name)
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
.setFontSize(28)
.setTranslation(25,35)
.setColor(1,0,1);
me.newText(me.model.name, me.style.font_color).setTranslation(25,35)
.setScale(me.style.scale_factor);
me.draw();
};
var draw = func {
var active = (getprop(me.options.current_wp_node) == me.model.idx);
if (active != me.active) {
me.path.setColor(
active ?
me.style.active_color :
me.style.inactive_color
);
me.active = active;
}
};

View file

@ -0,0 +1,76 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'WXR';
var parents = [SymbolLayer.Controller];
var __self__ = caller(0)[0];
SymbolLayer.Controller.add(name, __self__);
SymbolLayer.add(name, {
parents: [MultiSymbolLayer],
type: name, # Symbol type
df_controller: __self__, # controller to use by default -- this one
});
var new = func(layer) {
var m = {
parents: [__self__],
layer: layer,
map: layer.map,
listeners: [],
searchCmd: searchCmd_default,
};
layer.searcher._equals = func(l,r) l.equals(r);
m.addVisibilityListener();
return m;
};
var del = func() {
#print(name~".lcontroller.del()");
foreach (var l; me.listeners)
removelistener(l);
};
##
# dummy/placeholder (will be overridden in ctor and set to the default callback)
var searchCmd = func;
# FIXME: TFC/WXR seems only to be updated on range change/setPos currently ?
var searchCmd_default = func {
# print("WXR running");
if (me.map.getRange() == nil) return;
printlog(_MP_dbg_lvl, "Doing query: "~name);
var results = [];
foreach (var n; props.globals.getNode("/instrumentation/wxradar",1).getChildren("storm")) {
# Model 3 degree radar beam
var stormLat = n.getNode("latitude-deg").getValue();
var stormLon = n.getNode("longitude-deg").getValue();
print("processing storm at:",stormLat,'/',stormLon);
# FIXME: once ported to MapStructure, these should use the encapsulated "aircraft source"/driver stuff
var acLat = getprop("/position/latitude-deg");
var acLon = getprop("/position/longitude-deg");
var stormGeo = geo.Coord.new();
var acGeo = geo.Coord.new();
stormGeo.set_latlon(stormLat, stormLon);
acGeo.set_latlon(acLat, acLon);
var directDistance = acGeo.direct_distance_to(stormGeo);
var beamH = 0.1719 * directDistance; # M2FT * tan(3deg)
var beamBase = getprop("position/altitude-ft") - beamH;
if (n.getNode("top-altitude-ft").getValue() > beamBase) {
var tmp = geo.Coord.new();
tmp.set_latlon(stormLat, stormLon);
tmp.radiusNm = n.getNode("radius-nm").getValue();
tmp._node = n;
tmp.equals = func(r) me._node.equals(r._node);
append(results, tmp);
}
} # foreach
#print("WXR results:", size(results));
return results;
} # searchCmd_default

View file

@ -0,0 +1,23 @@
# See: http://wiki.flightgear.org/MapStructure
# Class things:
var name = 'WXR'; # storms/weather layer for LW/AW (use thunderstom scenario for testing)
var parents = [DotSym];
var __self__ = caller(0)[0];
DotSym.makeinstance( name, __self__ );
var element_type = "group"; # we want a group, becomes "me.element"
# TODO: how to integrate both styling and caching?
var draw = func {
# TODO: once this works properly, consider using caching here
# FIXME: scaling seems way off in comparison with the navdisplay - i.e. hard-coded assumptions about texture size/view port ?
me.element.createChild("image")
.setFile("Nasal/canvas/map/Images/storm.png")
.setSize(128*me.model.radiusNm,128*me.model.radiusNm)
.setTranslation(-64*me.model.radiusNm,-64*me.model.radiusNm)
.setCenter(0,0);
# .setScale(0.3);
# TODO: overlapping storms should probably set their z-index according to altitudes
};

View file

@ -18,12 +18,17 @@ SOURCES["main"] = {
# Layers which get updated every frame
var update_instant = [
"TFC",
"APS",
];
# Update at a slightly lower rate, but still
# unconditionally
var update_quickly = [
"TFC", "FLT",
];
var new = func(map, source='main') {
if (source != 'main')
die ("AI/MP traffic not yet supported (WIP)!");
if (!contains(SOURCES, source))
__die("AI/MP traffic not yet supported (WIP)!");
var m = {
parents: [__self__],
@ -32,42 +37,56 @@ var new = func(map, source='main') {
_pos: nil, _time: nil, _range: nil,
};
m.timer1 = maketimer(0, m, update_pos);
m.timer2 = maketimer(0, m, update_layers);
m.timer1.start();
m.timer2.start();
m.timer2 = maketimer(0.4, m, update_layers);
m.start();
m.update_pos();
return m;
};
var del = func(map) {
if (map != me.map) die();
var start = func() {
me.timer1.start();
me.timer2.start();
};
var stop = func() {
me.timer1.stop();
me.timer2.stop();
};
var del = func(map) {
if (map != me.map) die();
me.stop();
};
# Controller methods
var update_pos = func {
var (lat,lon) = me.source.getPosition();
me.map.setPos(lat:lat, lon:lon,
hdg:getprop("/orientation/heading-deg"),
hdg:me.source.getHeading(),
alt:me.source.getAltitude());
foreach (var t; update_instant)
if ((var l=me.map.getLayer(t)) != nil)
l.update();
};
var update_layers = func {
var do_all = 1;
var pos = me.map.getPosCoord();
var time = systime();
var range = me.map.getRange();
if (me._pos == nil)
me._pos = geo.Coord.new(pos);
# Always update if range changed
# FIXME: FIX doesn't update unless range is changed?
elsif (range == me._range) {
var dist_m = me._pos.direct_distance_to(pos);
# 2 NM until we update again
if (dist_m < 2 * NM2M) return;
if (dist_m < 2 * NM2M) do_all = 0;
# Update at most every 4 seconds to avoid excessive stutter:
elsif (time - me._time < 4) return;
elsif (time - me._time < 4) do_all = 0;
}
if (!do_all) {
foreach (var t; update_quickly)
if ((var l=me.map.getLayer(t)) != nil)
l.update();
return;
} else
printlog(_MP_dbg_lvl, "update aircraft position");
var (x,y,z) = pos.xyz();
me._pos.set_xyz(x,y,z);

View file

@ -1,35 +0,0 @@
# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure
###
#
#
var draw_dme = func (group, dme, controller=nil, lod=0) {
var lat = dme.lat;
var lon = dme.lon;
var name = dme.id;
var freq = dme.frequency;
var dme_grp = group.createChild("group","dme");
var icon_dme = dme_grp .createChild("path", name)
.moveTo(-15,0)
.line(-12.5,-7.5)
.line(7.5,-12.5)
.line(12.5,7.5)
.lineTo(7.5,-12.5)
.line(12.5,-7.5)
.line(7.5,12.5)
.line(-12.5,7.5)
.lineTo(15,0)
.lineTo(7.5,12.5)
.vert(14.5)
.horiz(-14.5)
.vert(-14.5)
.close()
.setStrokeLineWidth(3)
.setColor(0,0.6,0.85);
dme_grp.setGeoPosition(lat, lon)
.set("z-index",3);
if( controller != nil and controller['is_tuned'](freq))
icon_dme.setColor(0,1,0);
}

View file

@ -1,10 +0,0 @@
# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure
var DMELayer = {};
DMELayer.new = func(group,name,controller=nil) {
var m=Layer.new(group, name, DMEModel, controller);
m.setDraw (func draw_layer(layer:m, callback: draw_dme, lod:0) );
return m;
}
register_layer("dme", DMELayer);

View file

@ -1,21 +0,0 @@
# WARNING: *.model files will be deprecated, see: http://wiki.flightgear.org/MapStructure
var DMEModel = {};
DMEModel.new = func make( LayerModel, DMEModel );
DMEModel.init = func {
me._view.reset();
if(0) {
debug.dump( me._controller );
print(typeof(
me._controller.query_range()
));
}
var results = positioned.findWithinRange(me._controller.query_range() ,"dme");
foreach(result; results) {
me.push(result);
}
me.notifyView();
}

View file

@ -9,7 +9,7 @@ FixModel.init = func {
var numNum = 0;
foreach(result; results) {
# Skip airport fixes
if(string.match(result.id,"*[^0-9]")) {
if(string.match(result.id,"*[^0-9]")) { # this may need to be moved to FIX.lcontroller?
me.push(result);
numNum = numNum + 1;
}

View file

@ -3,605 +3,24 @@
# See: http://wiki.flightgear.org/Canvas_ND_Framework
# ==============================================================================
##
# do we really need to keep track of each drawable here ??
var i = 0;
##
# pseudo DSL-ish: use these as placeholders in the config hash below
var ALWAYS = func 1;
var NOTHING = func nil;
##
# so that we only need to update a single line ...
# this file contains a hash that declares features in a generic fashion
# we want to get rid of the bloated update() method sooner than later
# PLEASE DO NOT ADD any code to update() !!
# Instead, help clean up the file and move things over to the navdisplay.styles file
#
var trigger_update = func(layer) layer._model.init();
##
# TODO: move ND-specific implementation details into this lookup hash
# so that other aircraft and ND types can be more easily supported
# This is the only sane way to keep on generalizing the framework, so that we can
# also support different makes/models of NDs in the future
#
# any aircraft-specific ND behavior should be wrapped here,
# to isolate/decouple things in the generic NavDisplay class
# a huge bloated update() method is going to make that basically IMPOSSIBLE
#
# TODO: move this to an XML config file
#
var NDStyles = {
##
# this configures the 744 ND to help generalize the NavDisplay class itself
'Boeing': {
font_mapper: func(family, weight) {
if( family == "Liberation Sans" and weight == "normal" )
return "LiberationFonts/LiberationSans-Regular.ttf";
},
# where all the symbols are stored
# TODO: SVG elements should be renamed to use boeing/airbus prefix
# aircraft developers should all be editing the same ND.svg image
# the code can deal with the differences now
svg_filename: "Nasal/canvas/map/boeingND.svg",
##
## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map)
##
layers: [
{ name:'FIX', isMapStructure:1, update_on:['toggle_range','toggle_waypoints'],
# FIXME: this is a really ugly place for controller code
predicate: func(nd, layer) {
# print("Running fix layer predicate");
# toggle visibility here
var visible=nd.get_switch('toggle_waypoints') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
layer.group.setVisible( nd.get_switch('toggle_waypoints') );
if (visible) {
#print("Updating MapStructure ND layer: FIX");
# (Hopefully) smart update
layer.update();
}
}, # end of layer update predicate
}, # end of FIX layer
# Should redraw every 10 seconds
{ name:'storms', update_on:['toggle_range','toggle_weather','toggle_display_mode'],
predicate: func(nd, layer) {
# print("Running fixes predicate");
var visible=nd.get_switch('toggle_weather') and nd.get_switch('toggle_display_mode') != "PLAN";
if (visible) {
#print("storms update requested!");
trigger_update( layer );
}
layer._view.setVisible(visible);
}, # end of layer update predicate
}, # end of storms layer
{ name:'airplaneSymbol', disabled:1, update_on:['toggle_display_mode'],
predicate: func(nd, layer) {
var visible = nd.get_switch('toggle_display_mode') == "PLAN";
if (visible) {
trigger_update( layer );
} layer._view.setVisible(visible);
},
},
{ name:'APS', isMapStructure:1, update_on:['toggle_display_mode'],
predicate: func(nd, layer) {
var visible = nd.get_switch('toggle_display_mode') == "PLAN";
layer.group.setVisible( visible );
if (visible) {
layer.update();
}
},
},
{ name:'APT', isMapStructure:1, update_on:['toggle_range','toggle_airports','toggle_display_mode'],
predicate: func(nd, layer) {
# toggle visibility here
var visible=nd.get_switch('toggle_airports') and nd.in_mode('toggle_display_mode', ['MAP']);
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: APT");
layer.update();
}
}, # end of layer update predicate
}, # end of APT layer
# Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag.
{ name:'VOR', isMapStructure:1, update_on:['toggle_range','toggle_stations','toggle_display_mode'],
# FIXME: this is a really ugly place for controller code
predicate: func(nd, layer) {
# toggle visibility here
var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: VOR");
layer.update();
}
}, # end of layer update predicate
}, # end of VOR layer
{ name:'DME', isMapStructure:1, update_on:['toggle_range','toggle_stations'],
# FIXME: this is a really ugly place for controller code
predicate: func(nd, layer) {
var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
# toggle visibility here
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: DME");
layer.update();
}
}, # end of layer update predicate
}, # end of DME layer
{ name:'TFC', isMapStructure:1, update_on:['toggle_range','toggle_traffic'],
predicate: func(nd, layer) {
var visible = nd.get_switch('toggle_traffic');
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: TFC");
layer.update();
}
}, # end of layer update predicate
}, # end of traffic layer
{ name:'runway-nd', update_on:['toggle_range','toggle_display_mode'],
predicate: func(nd, layer) {
var visible = (nd.rangeNm() <= 40) and getprop("autopilot/route-manager/active") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']) ;
if (visible)
trigger_update( layer ); # clear & redraw
layer._view.setVisible( visible );
}, # end of layer update predicate
}, # end of airports-nd layer
{ name:'route', update_on:['toggle_range','toggle_display_mode'],
predicate: func(nd, layer) {
var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN']));
if (visible)
trigger_update( layer ); # clear & redraw
layer._view.setVisible( visible );
}, # end of layer update predicate
}, # end of route layer
## add other layers here, layer names must match the registered names as used in *.layer files for now
## this will all change once we're using Philosopher's MapStructure framework
], # end of vector with configured layers
# This is where SVG elements are configured by providing "behavior" hashes, i.e. for animations
# to animate each SVG symbol, specify behavior via callbacks (predicate, and true/false implementation)
# SVG identifier, callback etc
# TODO: update_on([]), update_mode (update() vs. timers/listeners)
# TODO: support putting symbols on specific layers
features: [
{
# TODO: taOnly doesn't need to use getprop polling in update(), use a listener instead!
id: 'taOnly', # the SVG ID
impl: { # implementation hash
init: func(nd, symbol), # for updateCenter stuff, called during initialization in the ctor
predicate: func(nd) getprop("instrumentation/tcas/inputs/mode") == 2, # the condition
is_true: func(nd) nd.symbols.taOnly.show(), # if true, run this
is_false: func(nd) nd.symbols.taOnly.hide(), # if false, run this
}, # end of taOnly behavior/callbacks
}, # end of taOnly
{
id: 'tas',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.aircraft_source.get_spd() > 100,
is_true: func(nd) {
nd.symbols.tas.setText(sprintf("%3.0f",getprop("/velocities/airspeed-kt") ));
nd.symbols.tas.show();
},
is_false: func(nd) nd.symbols.tas.hide(),
},
},
{
id: 'tasLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.aircraft_source.get_spd() > 100,
is_true: func(nd) nd.symbols.tasLbl.show(),
is_false: func(nd) nd.symbols.tasLbl.hide(),
},
},
{
id: 'ilsFreq',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']),
is_true: func(nd) {
nd.symbols.ilsFreq.show();
if(getprop("instrumentation/nav/in-range"))
nd.symbols.ilsFreq.setText(getprop("instrumentation/nav/nav-id"));
else
nd.symbols.ilsFreq.setText(getprop("instrumentation/nav/frequencies/selected-mhz-fmt"));
},
is_false: func(nd) nd.symbols.ilsFreq.hide(),
},
},
{
id: 'ilsLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']),
is_true: func(nd) {
nd.symbols.ilsLbl.show();
},
is_false: func(nd) nd.symbols.ilsLbl.hide(),
},
},
{
id: 'wpActiveId',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("/autopilot/route-manager/wp/id") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
nd.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id"));
nd.symbols.wpActiveId.show();
},
is_false: func(nd) nd.symbols.wpActiveId.hide(),
}, # of wpActiveId.impl
}, # of wpActiveId
{
id: 'wpActiveDist',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
nd.symbols.wpActiveDist.setText(sprintf("%3.01f",getprop("/autopilot/route-manager/wp/dist")));
nd.symbols.wpActiveDist.show();
},
is_false: func(nd) nd.symbols.wpActiveDist.hide(),
},
},
{
id: 'wpActiveDistLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
nd.symbols.wpActiveDistLbl.show();
if(getprop("/autopilot/route-manager/wp/dist") > 1000)
nd.symbols.wpActiveDistLbl.setText(" NM");
},
is_false: func(nd) nd.symbols.wpActiveDistLbl.hide(),
},
},
{
id: 'eta',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
var etaSec = getprop("/sim/time/utc/day-seconds")+getprop("autopilot/route-manager/wp/eta-seconds");
var h = math.floor(etaSec/3600);
etaSec=etaSec-3600*h;
var m = math.floor(etaSec/60);
etaSec=etaSec-60*m;
var s = etaSec/10;
if (h>24) h=h-24;
nd.symbols.eta.setText(sprintf("%02.0f%02.0f.%01.0fz",h,m,s));
nd.symbols.eta.show();
},
is_false: func(nd) nd.symbols.eta.hide(),
}, # of eta.impl
}, # of eta
{
id: 'gsGroup',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']),
is_true: func(nd) {
if(nd.get_switch('toggle_centered'))
nd.symbols.gsGroup.setTranslation(0,0);
else
nd.symbols.gsGroup.setTranslation(0,150);
nd.symbols.gsGroup.show();
},
is_false: func(nd) nd.symbols.gsGroup.hide(),
},
},
{
id:'hdg',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','MAP','VOR']),
is_true: func(nd) {
var hdgText = "";
if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
{
if(nd.get_switch('toggle_true_north'))
hdgText = nd.aircraft_source.get_trk_tru();
else
hdgText = nd.aircraft_source.get_trk_mag();
} else {
if(nd.get_switch('toggle_true_north'))
hdgText = nd.aircraft_source.get_hdg_tru();
else
hdgText = nd.aircraft_source.get_hdg_mag();
}
if(hdgText < 0.5) hdgText = 360 + hdgText;
elsif(hdgText >= 360.5) hdgText = hdgText - 360;
nd.symbols.hdg.setText(sprintf("%03.0f", hdgText));
},
is_false: NOTHING,
},
},
{
id:'hdgGroup',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','MAP','VOR']),
is_true: func(nd) {
nd.symbols.hdgGroup.show();
if(nd.get_switch('toggle_centered'))
nd.symbols.hdgGroup.setTranslation(0,100);
else
nd.symbols.hdgGroup.setTranslation(0,0);
},
is_false: func(nd) nd.symbols.hdgGroup.hide(),
},
},
{
id:'gs',
impl: {
init: func(nd,symbol),
common: func(nd) nd.symbols.gs.setText(sprintf("%3.0f",nd.aircraft_source.get_gnd_spd() )),
predicate: func(nd) nd.aircraft_source.get_gnd_spd() >= 30,
is_true: func(nd) {
nd.symbols.gs.setFontSize(36);
},
is_false: func(nd) nd.symbols.gs.setFontSize(52),
},
},
{
id:'rangeArcs',
impl: {
init: func(nd,symbol),
predicate: func(nd) !nd.get_switch('toggle_centered') and nd.get_switch('toggle_rangearc'),
is_true: func(nd) nd.symbols.rangeArcs.show(),
is_false: func(nd) nd.symbols.rangeArcs.hide(),
}, # of rangeArcs.impl
}, # of rangeArcs
{
id:'rangePln1',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln1.show();
nd.symbols.rangePln1.setText(sprintf("%3.0f",nd.rangeNm()));
},
is_false: func(nd) nd.symbols.rangePln1.hide(),
},
},
{
id:'rangePln2',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln2.show();
nd.symbols.rangePln2.setText(sprintf("%3.0f",nd.rangeNm()/2));
},
is_false: func(nd) nd.symbols.rangePln2.hide(),
},
},
{
id:'rangePln3',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln3.show();
nd.symbols.rangePln3.setText(sprintf("%3.0f",nd.rangeNm()/2));
},
is_false: func(nd) nd.symbols.rangePln3.hide(),
},
},
{
id:'rangePln4',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln4.show();
nd.symbols.rangePln4.setText(sprintf("%3.0f",nd.rangeNm()));
},
is_false: func(nd) nd.symbols.rangePln4.hide(),
},
},
{
id:'crsLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) nd.symbols.crsLbl.show(),
is_false: func(nd) nd.symbols.crsLbl.hide(),
},
},
{
id:'crs',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) {
nd.symbols.crs.show();
if(getprop("instrumentation/nav/radials/selected-deg") != nil)
nd.symbols.crs.setText(sprintf("%03.0f",getprop("instrumentation/nav/radials/selected-deg")));
},
is_false: func(nd) nd.symbols.crs.hide(),
},
},
{
id:'dmeLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) nd.symbols.dmeLbl.show(),
is_false: func(nd) nd.symbols.dmeLbl.hide(),
},
},
{
id:'dme',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) {
nd.symbols.dme.show();
if(getprop("instrumentation/dme/in-range"))
nd.symbols.dme.setText(sprintf("%3.1f",getprop("instrumentation/dme/indicated-distance-nm")));
},
is_false: func(nd) nd.symbols.dme.hide(),
},
},
{
id:'trkInd2',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['MAP','APP','VOR']) and nd.get_switch('toggle_centered')),
is_true: func(nd) {
nd.symbols.trkInd2.show();
},
is_false: func(nd) nd.symbols.trkInd2.hide(),
},
},
{
id:'vorCrsPtr',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and !nd.get_switch('toggle_centered')),
is_true: func(nd) {
nd.symbols.vorCrsPtr.show();
if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
nd.symbols.vorCrsPtr.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_trk_mag())*D2R);
else
nd.symbols.vorCrsPtr.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_hdg_mag())*D2R);
},
is_false: func(nd) nd.symbols.vorCrsPtr.hide(),
},
},
{
id:'vorCrsPtr2',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and nd.get_switch('toggle_centered')),
is_true: func(nd) {
nd.symbols.vorCrsPtr2.show();
if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
nd.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_trk_mag())*D2R);
else
nd.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_hdg_mag())*D2R);
},
is_false: func(nd) nd.symbols.vorCrsPtr2.hide(),
},
},
{
id: 'gsDiamond',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']) and getprop("instrumentation/nav/gs-in-range"),
is_true: func(nd) {
var gs_deflection = getprop("instrumentation/nav/gs-needle-deflection-norm");
if(gs_deflection != nil)
nd.symbols.gsDiamond.setTranslation(gs_deflection*150,0);
if(abs(gs_deflection) < 0.99)
nd.symbols.gsDiamond.setColorFill(1,0,1,1);
else
nd.symbols.gsDiamond.setColorFill(0,0,0,1);
},
is_false: func(nd) nd.symbols.gsGroup.hide(),
},
},
{
id:'locPtr',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and !nd.get_switch('toggle_centered') and getprop("instrumentation/nav/in-range")),
is_true: func(nd) {
nd.symbols.locPtr.show();
var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm");
nd.symbols.locPtr.setTranslation(deflection*150,0);
if(abs(deflection) < 0.99)
nd.symbols.locPtr.setColorFill(1,0,1,1);
else
nd.symbols.locPtr.setColorFill(0,0,0,1);
},
is_false: func(nd) nd.symbols.locPtr.hide(),
},
},
{
id:'locPtr2',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and nd.get_switch('toggle_centered') and getprop("instrumentation/nav/in-range")),
is_true: func(nd) {
nd.symbols.locPtr2.show();
var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm");
nd.symbols.locPtr2.setTranslation(deflection*150,0);
if(abs(deflection) < 0.99)
nd.symbols.locPtr2.setColorFill(1,0,1,1);
else
nd.symbols.locPtr2.setColorFill(0,0,0,1);
},
is_false: func(nd) nd.symbols.locPtr2.hide(),
},
},
{
id:'wind',
impl: {
init: func(nd,symbol),
predicate: ALWAYS,
is_true: func(nd) {
var windDir = getprop("environment/wind-from-heading-deg");
if(!nd.get_switch('toggle_true_north'))
windDir = windDir - getprop("environment/magnetic-variation-deg");
if(windDir < 0.5) windDir = 360 + windDir;
elsif(windDir >= 360.5) windDir = windDir - 360;
nd.symbols.wind.setText(sprintf("%03.0f / %02.0f",windDir,getprop("environment/wind-speed-kt")));
},
is_false: NOTHING,
},
},
{
id:'windArrow',
impl: {
init: func(nd,symbol),
predicate: func(nd) (!(nd.in_mode('toggle_display_mode', ['PLAN']) and (nd.get_switch('toggle_display_type') == "LCD")) and nd.aircraft_source.get_spd() > 100),
is_true: func(nd) {
nd.symbols.windArrow.show();
var windArrowRot = getprop("environment/wind-from-heading-deg");
if((nd.in_mode('toggle_display_mode', ['MAP','PLAN']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
windArrowRot = windArrowRot - nd.aircraft_source.get_trk_mag();
else
windArrowRot = windArrowRot - nd.aircraft_source.get_hdg_mag();
nd.symbols.windArrow.setRotation(windArrowRot*D2R);
},
is_false: func(nd) nd.symbols.windArrow.hide(),
},
},
], # end of vector with features
}, # end of Boeing style
#####
##
## add support for other aircraft/ND types and styles here (Airbus etc)
##
##
}; # end of NDStyles
io.include("Nasal/canvas/map/navdisplay.styles");
##
# encapsulate hdg/lat/lon source, so that the ND may also display AI/MP aircraft in a pilot-view at some point (aka stress-testing)
#
# TODO: this predates aircraftpos.controller (MapStructure) should probably be unified to some degree ...
var NDSourceDriver = {};
NDSourceDriver.new = func {
@ -666,17 +85,9 @@ var default_switches = {
'toggle_display_type': {path: '/mfd/display-type', value:'CRT', type:'STRING'}, # valid values are: CRT or LCD
'toggle_true_north': {path: '/mfd/true-north', value:0, type:'BOOL'},
'toggle_rangearc': {path: '/mfd/rangearc', value:0, type:'BOOL'},
'toggle_track_heading': {path: '/trk-selected', value:0, type:'BOOL'},
'toggle_track_heading':{path: '/trk-selected', value:0, type:'BOOL'},
};
# Hack to update weather radar once every 10 seconds
var update_weather = func {
if (getprop("/instrumentation/efis/inputs/wxr") != nil)
setprop("/instrumentation/efis/inputs/wxr",getprop("/instrumentation/efis/inputs/wxr"));
settimer(update_weather, 10);
}
update_weather();
##
# TODO:
# - introduce a MFD class (use it also for PFD/EICAS)
@ -690,6 +101,8 @@ var NavDisplay = {
print("Cleaning up NavDisplay");
# shut down all timers and other loops here
me.update_timer.stop();
foreach(var t; me.timers)
t.stop();
foreach(var l; me.listeners)
removelistener(l);
# clean up MapStructure
@ -701,6 +114,11 @@ var NavDisplay = {
NavDisplay.id -= 1;
},
addtimer: func(interval, cb) {
append(me.timers, var job=maketimer(interval, cb));
return job; # so that we can directly work with the timer (start/stop)
},
listen: func(p,c) {
append(me.listeners, setlistener(p,c));
},
@ -741,6 +159,8 @@ var NavDisplay = {
};
}, # of connectAI
setTimerInterval: func(update_time=0.05) me.update_timer.restart(update_time),
# TODO: the ctor should allow customization, for different aircraft
# especially properties and SVG files/handles (747, 757, 777 etc)
new : func(prop1, switches=default_switches, style='Boeing') {
@ -749,6 +169,7 @@ var NavDisplay = {
m.inited = 0;
m.timers=[];
m.listeners=[]; # for cleanup handling
m.aircraft_source = NDSourceDriver.new(); # uses the main aircraft as the driver/source (speeds, position, heading)
@ -799,12 +220,11 @@ var NavDisplay = {
return m;
},
newMFD: func(canvas_group, parent=nil, options=nil)
newMFD: func(canvas_group, parent=nil, options=nil, update_time=0.05)
{
if (me.inited) die("MFD already was added to scene");
me.inited = 1;
#me.listen("/sim/signals/reinit", func(n) me.del() );
me.update_timer = maketimer(0.05, func me.update() ); # TODO: make interval configurable via ctor
me.update_timer = maketimer(update_time, func me.update() );
me.nd = canvas_group;
me.canvas_handle = parent;
@ -814,7 +234,6 @@ var NavDisplay = {
me.symbols = {}; # storage for SVG elements, to avoid namespace pollution (all SVG elements end up here)
foreach(var feature; me.nd_style.features ) {
# print("Setting up SVG feature:", feature.id);
me.symbols[feature.id] = me.nd.getElementById(feature.id).updateCenter();
if(contains(feature.impl,'init')) feature.impl.init(me.nd, feature); # call The element's init code (i.e. updateCenter)
}
@ -839,10 +258,9 @@ var NavDisplay = {
me.map = me.nd.createChild("map","map")
.set("clip", "rect(124, 1024, 1024, 0)")
.set("screen-range", "700");
.set("screen-range", 700);
# this callback will be passed onto the model via the controller hash, and used for the positioned queries, to specify max query range:
var get_range = func me.get_switch('toggle_range');
me.update_sub(); # init some map properties based on switches
# predicate for the draw controller
var is_tuned = func(freq) {
@ -876,7 +294,6 @@ var NavDisplay = {
var controller = {
parents: [canvas.Map.Controller],
_pos: nil, _time: nil,
query_range: func get_range(),
is_tuned:is_tuned,
get_tuned_course:get_course_by_freq,
get_position: get_current_position,
@ -919,18 +336,31 @@ var NavDisplay = {
foreach(var layer; me.nd_style.layers) {
if(layer['disabled']) continue; # skip this layer
#print("newMFD(): Setting up ND layer:", layer.name);
# huge hack for the alt-arc, which is not rendered as a map group, but directly as part of the toplevel ND group
var render_target = (!contains(layer,'not_a_map') or !layer.not_a_map) ? me.map : me.nd;
var the_layer = nil;
if(!layer['isMapStructure']) # set up an old layer
the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller );
if(!layer['isMapStructure']) # set up an old INEFFICIENT and SLOW layer
the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( me.map, layer.name, controller );
else {
printlog(_MP_dbg_lvl, "Setting up MapStructure-based layer for ND, name:", layer.name);
var opt = options != nil and options[layer.name] != nil ? options[layer.name] :nil;
# print("Options is: ", opt!=nil?"enabled":"disabled");
render_target.addLayer(factory: canvas.SymbolLayer, type_arg: layer.name, options:opt);
the_layer = me.layers[layer.name] = render_target.getLayer(layer.name);
me.map.addLayer(
factory: canvas.SymbolLayer,
type_arg: layer.name,
options:opt,
visible:0,
priority: layer['z-index']
);
the_layer = me.layers[layer.name] = me.map.getLayer(layer.name);
if (1) (func {
var l = layer;
var _predicate = l.predicate;
l.predicate = func {
var t = systime();
call(_predicate, arg, me);
printlog(_MP_dbg_lvl, "Took "~((systime()-t)*1000)~"ms to update layer "~l.name);
}
})();
}
# now register all layer specific notification listeners and their corresponding update predicate/callback
@ -938,6 +368,14 @@ var NavDisplay = {
# so that it can directly access the ND instance and its own layer (without having to know the layer's name)
var event_handler = make_event_handler(layer.predicate, the_layer);
foreach(var event; layer.update_on) {
# this handles timers
if (typeof(event)=='hash' and contains(event, 'rate_hz')) {
#print("FIXME: navdisplay.mfd timer handling is broken ATM");
var job=me.addtimer(1/event.rate_hz, event_handler);
job.start();
}
# and this listeners
else
# print("Setting up subscription:", event, " for ", layer.name, " handler id:", id(event_handler) );
me.listen_switch(event, event_handler);
} # foreach event subscription
@ -950,17 +388,7 @@ var NavDisplay = {
# start the update timer, which makes sure that the update() will be called
me.update_timer.start();
# next, radio & autopilot & listeners
# TODO: move this to .init field in layers hash or to model files
foreach(var n; var radios = [
"instrumentation/nav/frequencies/selected-mhz",
"instrumentation/nav[1]/frequencies/selected-mhz"])
me.listen(n, func() {
# me.drawvor();
# me.drawdme();
});
# TODO: move this to the route.model
# TODO: move this to RTE.lcontroller ?
me.listen("/autopilot/route-manager/current-wp", func(activeWp) {
canvas.updatewp( activeWp.getValue() );
});
@ -972,13 +400,10 @@ var NavDisplay = {
foreach(var m; modes) if(me.get_switch(switch)==m) return 1;
return 0;
},
# each model should keep track of when it last got updated, using current lat/lon
# in update(), we can then check if the aircraft has traveled more than 0.5-1 nm (depending on selected range)
# and update each model accordingly
update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft
# Helper function for below (update()) and above (newMFD())
# to ensure position etc. are correct.
update_sub: func()
{
var _time = systime();
# Variables:
var userLat = me.aircraft_source.get_lat();
var userLon = me.aircraft_source.get_lon();
@ -1044,12 +469,23 @@ var NavDisplay = {
pos.lon = userLon;
}
call(me.map.setPos, [pos.lat, pos.lon], me.map, pos);
},
# each model should keep track of when it last got updated, using current lat/lon
# in update(), we can then check if the aircraft has traveled more than 0.5-1 nm (depending on selected range)
# and update each model accordingly
# TODO: Hooray is still waiting for a really rainy weekend to clean up all the mess here... so plz don't add to it!
update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft
{
var _time = systime();
call(me.update_sub, nil, nil, caller(0)[0]); # call this in the same namespace to "steal" its variables
# MapStructure update!
if (me.map.controller.should_update_all()) {
me.map.update();
} else {
# TODO: ugly list here
# FIXME: use a VOLATILE layer helper here that handles TFC, APS, WXR etc ?
me.map.update(func(layer) (var n=layer.type) == "TFC" or n == "APS");
}
@ -1345,8 +781,7 @@ var NavDisplay = {
me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports') and me.in_mode('toggle_display_mode', ['MAP']));
me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') and me.in_mode('toggle_display_mode', ['MAP']));
# Okay, _how_ do we hook this up with FGPlot?
#print("ND update took "~(systime()-_time)~" seconds");
printlog(_MP_dbg_lvl, "Total ND update took "~((systime()-_time)*100)~"ms");
setprop("/instrumentation/navdisplay["~ NavDisplay.id ~"]/update-ms", systime() - _time);
}
} # of update() method (50% of our file ...seriously?)
};

View file

@ -0,0 +1,619 @@
# ==============================================================================
# Boeing Navigation Display by Gijs de Rooy
# See: http://wiki.flightgear.org/Canvas_ND_Framework
# ==============================================================================
# This file makes use of the MapStructure framework, see: http://wiki.flightgear.org/Canvas_MapStructure
#
# Sooner or later, some parts will be revamped by coming up with a simple animation framework: http://wiki.flightgear.org/NavDisplay#mapping_vs._SVG_animation
##
# pseudo DSL-ish: use these as placeholders in the config hash below
var ALWAYS = func 1;
var NOTHING = func nil;
##
# TODO: move ND-specific implementation details into this lookup hash
# so that other aircraft and ND types can be more easily supported
#
# any aircraft-specific ND behavior should be wrapped here,
# to isolate/decouple things in the generic NavDisplay class
#
# TODO: move this to an XML config file (maybe supporting SGCondition and/or SGStateMachine markup for the logic?)
#
var NDStyles = {
##
# this configures the 744 ND to help generalize the NavDisplay class itself
'Boeing': {
font_mapper: func(family, weight) {
if( family == "Liberation Sans" and weight == "normal" )
return "LiberationFonts/LiberationSans-Regular.ttf";
},
# where all the symbols are stored
# TODO: SVG elements should be renamed to use boeing/airbus prefix
# aircraft developers should all be editing the same ND.svg image
# the code can deal with the differences now
svg_filename: "Nasal/canvas/map/Images/boeingND.svg",
##
## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map)
##
# TODO: phase out isMapStructure flag once map.nas & *.draw files are killed
layers: [
# TODO: take z-indices from *.draw files -- now handled by MapStructure in the addLayer method.
{ name:'FIX', isMapStructure:1, update_on:['toggle_range','toggle_waypoints'],
# FIXME: this is a really ugly place for controller code
predicate: func(nd, layer) {
# print("Running fix layer predicate");
# toggle visibility here
var visible=nd.get_switch('toggle_waypoints') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
layer.group.setVisible( nd.get_switch('toggle_waypoints') );
if (visible) {
#print("Updating MapStructure ND layer: FIX");
# (Hopefully) smart update
layer.update();
}
}, # end of layer update predicate
'z-index': 3,
}, # end of FIX layer
# Should redraw every 10 seconds TODO: use new MapStructure/WXR here once that works properly (Gijs should check first!)
{ name:'WXR', isMapStructure:1, update_on:[ {rate_hz: 0.1}, 'toggle_range','toggle_weather','toggle_display_mode'],
predicate: func(nd, layer) {
#print("Running storms predicate");
var visible=nd.get_switch('toggle_weather') and nd.get_switch('toggle_display_mode') != "PLAN";
layer.group.setVisible(visible);
if (visible) {
print("storms update requested! (timer issue when closing the dialog?)");
layer.update();
}
}, # end of layer update predicate
}, # end of storms/WXR layer
{ name:'APS', isMapStructure:1, update_on:['toggle_display_mode'],
predicate: func(nd, layer) {
var visible = nd.get_switch('toggle_display_mode') == "PLAN";
layer.group.setVisible( visible );
if (visible) {
layer.update();
}
},
},
{ name:'APT', isMapStructure:1, update_on:['toggle_range','toggle_airports','toggle_display_mode'],
predicate: func(nd, layer) {
# toggle visibility here
var visible=nd.get_switch('toggle_airports') and nd.in_mode('toggle_display_mode', ['MAP']);
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: APT");
layer.update();
}
}, # end of layer update predicate
'z-index': 1,
}, # end of APT layer
# Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag.
{ name:'VOR', isMapStructure:1, update_on:['toggle_range','toggle_stations','toggle_display_mode'],
# FIXME: this is a really ugly place for controller code
predicate: func(nd, layer) {
# toggle visibility here
var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: VOR");
layer.update();
}
}, # end of layer update predicate
'z-index': 3,
}, # end of VOR layer
{ name:'DME', isMapStructure:1, update_on:['toggle_range','toggle_stations'],
# FIXME: this is a really ugly place for controller code
predicate: func(nd, layer) {
var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
# toggle visibility here
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: DME");
layer.update();
}
}, # end of layer update predicate
'z-index': 3,
}, # end of DME layer
{ name:'TFC', isMapStructure:1, update_on:['toggle_range','toggle_traffic'],
predicate: func(nd, layer) {
var visible = nd.get_switch('toggle_traffic');
layer.group.setVisible( visible );
if (visible) {
#print("Updating MapStructure ND layer: TFC");
layer.update();
}
}, # end of layer update predicate
'z-index': 1,
}, # end of traffic layer
{ name:'runway-nd', update_on:['toggle_range','toggle_display_mode'],
predicate: func(nd, layer) {
#print("runway-nd wants to be ported to MapStructure");
var visible = (nd.rangeNm() <= 40) and getprop("autopilot/route-manager/active") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']) ;
if (visible)
layer._model.init(); # clear & redraw
layer._view.setVisible( visible );
}, # end of layer update predicate
}, # end of airports-nd layer
{ name:'RTE', isMapStructure:1, update_on:['toggle_range','toggle_display_mode'],
predicate: func(nd, layer) {
var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN']));
layer.group.setVisible( visible );
if (visible)
layer.update();
}, # end of layer update predicate
'z-index': 2, # apparently route.draw doesn't have a z-index?
}, # end of route layer
{ name:'WPT', isMapStructure:1, update_on:['toggle_range','toggle_display_mode'],
predicate: func(nd, layer) {
var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN']));
layer.group.setVisible( visible );
if (visible)
layer.update();
}, # end of layer update predicate
'z-index': 4,
}, # end of waypoint layer
{ name:'ALT-profile', isMapStructure:1, update_on:['toggle_range','toggle_display_mode'],
predicate: func(nd, layer) {
var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN']));
layer.group.setVisible( visible );
if (visible)
layer.update();
}, # end of layer update predicate
'z-index': 4,
}, # end of altitude profile layer
## add other layers here, layer names must match the registered names as used in *.layer files for now
## this will all change once we're using Philosopher's MapStructure framework
], # end of vector with configured layers
# This is where SVG elements are configured by providing "behavior" hashes, i.e. for animations
# to animate each SVG symbol, specify behavior via callbacks (predicate, and true/false implementation)
# SVG identifier, callback etc
# TODO: update_on([]), update_mode (update() vs. timers/listeners)
# TODO: support putting symbols on specific layers
features: [
{
# TODO: taOnly doesn't need to use getprop polling in update(), use a listener instead!
id: 'taOnly', # the SVG ID
impl: { # implementation hash
init: func(nd, symbol), # for updateCenter stuff, called during initialization in the ctor
predicate: func(nd) getprop("instrumentation/tcas/inputs/mode") == 2, # the condition
is_true: func(nd) nd.symbols.taOnly.show(), # if true, run this
is_false: func(nd) nd.symbols.taOnly.hide(), # if false, run this
}, # end of taOnly behavior/callbacks
}, # end of taOnly
{
id: 'tas',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.aircraft_source.get_spd() > 100,
is_true: func(nd) {
nd.symbols.tas.setText(sprintf("%3.0f",getprop("/velocities/airspeed-kt") ));
nd.symbols.tas.show();
},
is_false: func(nd) nd.symbols.tas.hide(),
},
},
{
id: 'tasLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.aircraft_source.get_spd() > 100,
is_true: func(nd) nd.symbols.tasLbl.show(),
is_false: func(nd) nd.symbols.tasLbl.hide(),
},
},
{
id: 'ilsFreq',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']),
is_true: func(nd) {
nd.symbols.ilsFreq.show();
if(getprop("instrumentation/nav/in-range"))
nd.symbols.ilsFreq.setText(getprop("instrumentation/nav/nav-id"));
else
nd.symbols.ilsFreq.setText(getprop("instrumentation/nav/frequencies/selected-mhz-fmt"));
},
is_false: func(nd) nd.symbols.ilsFreq.hide(),
},
},
{
id: 'ilsLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']),
is_true: func(nd) {
nd.symbols.ilsLbl.show();
},
is_false: func(nd) nd.symbols.ilsLbl.hide(),
},
},
{
id: 'wpActiveId',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("/autopilot/route-manager/wp/id") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
nd.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id"));
nd.symbols.wpActiveId.show();
},
is_false: func(nd) nd.symbols.wpActiveId.hide(),
}, # of wpActiveId.impl
}, # of wpActiveId
{
id: 'wpActiveDist',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
nd.symbols.wpActiveDist.setText(sprintf("%3.01f",getprop("/autopilot/route-manager/wp/dist")));
nd.symbols.wpActiveDist.show();
},
is_false: func(nd) nd.symbols.wpActiveDist.hide(),
},
},
{
id: 'wpActiveDistLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
nd.symbols.wpActiveDistLbl.show();
if(getprop("/autopilot/route-manager/wp/dist") > 1000)
nd.symbols.wpActiveDistLbl.setText(" NM");
},
is_false: func(nd) nd.symbols.wpActiveDistLbl.hide(),
},
},
{
id: 'eta',
impl: {
init: func(nd,symbol),
predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil and getprop("autopilot/route-manager/active")
and nd.in_mode('toggle_display_mode', ['MAP','PLAN']),
is_true: func(nd) {
var etaSec = getprop("/sim/time/utc/day-seconds")+getprop("autopilot/route-manager/wp/eta-seconds");
var h = math.floor(etaSec/3600);
etaSec=etaSec-3600*h;
var m = math.floor(etaSec/60);
etaSec=etaSec-60*m;
var s = etaSec/10;
if (h>24) h=h-24;
nd.symbols.eta.setText(sprintf("%02.0f%02.0f.%01.0fz",h,m,s));
nd.symbols.eta.show();
},
is_false: func(nd) nd.symbols.eta.hide(),
}, # of eta.impl
}, # of eta
{
id: 'gsGroup',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']),
is_true: func(nd) {
if(nd.get_switch('toggle_centered'))
nd.symbols.gsGroup.setTranslation(0,0);
else
nd.symbols.gsGroup.setTranslation(0,150);
nd.symbols.gsGroup.show();
},
is_false: func(nd) nd.symbols.gsGroup.hide(),
},
},
{
id:'hdg',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','MAP','VOR']),
is_true: func(nd) {
var hdgText = "";
if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
{
if(nd.get_switch('toggle_true_north'))
hdgText = nd.aircraft_source.get_trk_tru();
else
hdgText = nd.aircraft_source.get_trk_mag();
} else {
if(nd.get_switch('toggle_true_north'))
hdgText = nd.aircraft_source.get_hdg_tru();
else
hdgText = nd.aircraft_source.get_hdg_mag();
}
if(hdgText < 0.5) hdgText = 360 + hdgText;
elsif(hdgText >= 360.5) hdgText = hdgText - 360;
nd.symbols.hdg.setText(sprintf("%03.0f", hdgText));
},
is_false: NOTHING,
},
},
{
id:'hdgGroup',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','MAP','VOR']),
is_true: func(nd) {
nd.symbols.hdgGroup.show();
if(nd.get_switch('toggle_centered'))
nd.symbols.hdgGroup.setTranslation(0,100);
else
nd.symbols.hdgGroup.setTranslation(0,0);
},
is_false: func(nd) nd.symbols.hdgGroup.hide(),
},
},
{
id:'gs',
impl: {
init: func(nd,symbol),
common: func(nd) nd.symbols.gs.setText(sprintf("%3.0f",nd.aircraft_source.get_gnd_spd() )),
predicate: func(nd) nd.aircraft_source.get_gnd_spd() >= 30,
is_true: func(nd) {
nd.symbols.gs.setFontSize(36);
},
is_false: func(nd) nd.symbols.gs.setFontSize(52),
},
},
{
id:'rangeArcs',
impl: {
init: func(nd,symbol),
predicate: func(nd) !nd.get_switch('toggle_centered') and nd.get_switch('toggle_rangearc'),
is_true: func(nd) nd.symbols.rangeArcs.show(),
is_false: func(nd) nd.symbols.rangeArcs.hide(),
}, # of rangeArcs.impl
}, # of rangeArcs
{
id:'rangePln1',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln1.show();
nd.symbols.rangePln1.setText(sprintf("%3.0f",nd.rangeNm()));
},
is_false: func(nd) nd.symbols.rangePln1.hide(),
},
},
{
id:'rangePln2',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln2.show();
nd.symbols.rangePln2.setText(sprintf("%3.0f",nd.rangeNm()/2));
},
is_false: func(nd) nd.symbols.rangePln2.hide(),
},
},
{
id:'rangePln3',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln3.show();
nd.symbols.rangePln3.setText(sprintf("%3.0f",nd.rangeNm()/2));
},
is_false: func(nd) nd.symbols.rangePln3.hide(),
},
},
{
id:'rangePln4',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN",
is_true: func(nd) {
nd.symbols.rangePln4.show();
nd.symbols.rangePln4.setText(sprintf("%3.0f",nd.rangeNm()));
},
is_false: func(nd) nd.symbols.rangePln4.hide(),
},
},
{
id:'crsLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) nd.symbols.crsLbl.show(),
is_false: func(nd) nd.symbols.crsLbl.hide(),
},
},
{
id:'crs',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) {
nd.symbols.crs.show();
if(getprop("instrumentation/nav/radials/selected-deg") != nil)
nd.symbols.crs.setText(sprintf("%03.0f",getprop("instrumentation/nav/radials/selected-deg")));
},
is_false: func(nd) nd.symbols.crs.hide(),
},
},
{
id:'dmeLbl',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) nd.symbols.dmeLbl.show(),
is_false: func(nd) nd.symbols.dmeLbl.hide(),
},
},
{
id:'dme',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']),
is_true: func(nd) {
nd.symbols.dme.show();
if(getprop("instrumentation/dme/in-range"))
nd.symbols.dme.setText(sprintf("%3.1f",getprop("instrumentation/dme/indicated-distance-nm")));
},
is_false: func(nd) nd.symbols.dme.hide(),
},
},
{
id:'trkInd2',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['MAP','APP','VOR']) and nd.get_switch('toggle_centered')),
is_true: func(nd) {
nd.symbols.trkInd2.show();
},
is_false: func(nd) nd.symbols.trkInd2.hide(),
},
},
{
id:'vorCrsPtr',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and !nd.get_switch('toggle_centered')),
is_true: func(nd) {
nd.symbols.vorCrsPtr.show();
if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
nd.symbols.vorCrsPtr.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_trk_mag())*D2R);
else
nd.symbols.vorCrsPtr.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_hdg_mag())*D2R);
},
is_false: func(nd) nd.symbols.vorCrsPtr.hide(),
},
},
{
id:'vorCrsPtr2',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and nd.get_switch('toggle_centered')),
is_true: func(nd) {
nd.symbols.vorCrsPtr2.show();
if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
nd.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_trk_mag())*D2R);
else
nd.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_hdg_mag())*D2R);
},
is_false: func(nd) nd.symbols.vorCrsPtr2.hide(),
},
},
{
id: 'gsDiamond',
impl: {
init: func(nd,symbol),
predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']) and getprop("instrumentation/nav/gs-in-range"),
is_true: func(nd) {
var gs_deflection = getprop("instrumentation/nav/gs-needle-deflection-norm");
if(gs_deflection != nil)
nd.symbols.gsDiamond.setTranslation(gs_deflection*150,0);
if(abs(gs_deflection) < 0.99)
nd.symbols.gsDiamond.setColorFill(1,0,1,1);
else
nd.symbols.gsDiamond.setColorFill(0,0,0,1);
},
is_false: func(nd) nd.symbols.gsGroup.hide(),
},
},
{
id:'locPtr',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and !nd.get_switch('toggle_centered') and getprop("instrumentation/nav/in-range")),
is_true: func(nd) {
nd.symbols.locPtr.show();
var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm");
nd.symbols.locPtr.setTranslation(deflection*150,0);
if(abs(deflection) < 0.99)
nd.symbols.locPtr.setColorFill(1,0,1,1);
else
nd.symbols.locPtr.setColorFill(0,0,0,1);
},
is_false: func(nd) nd.symbols.locPtr.hide(),
},
},
{
id:'locPtr2',
impl: {
init: func(nd,symbol),
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and nd.get_switch('toggle_centered') and getprop("instrumentation/nav/in-range")),
is_true: func(nd) {
nd.symbols.locPtr2.show();
var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm");
nd.symbols.locPtr2.setTranslation(deflection*150,0);
if(abs(deflection) < 0.99)
nd.symbols.locPtr2.setColorFill(1,0,1,1);
else
nd.symbols.locPtr2.setColorFill(0,0,0,1);
},
is_false: func(nd) nd.symbols.locPtr2.hide(),
},
},
{
id:'wind',
impl: {
init: func(nd,symbol),
predicate: ALWAYS,
is_true: func(nd) {
var windDir = getprop("environment/wind-from-heading-deg");
if(!nd.get_switch('toggle_true_north'))
windDir = windDir - getprop("environment/magnetic-variation-deg");
if(windDir < 0.5) windDir = 360 + windDir;
elsif(windDir >= 360.5) windDir = windDir - 360;
nd.symbols.wind.setText(sprintf("%03.0f / %02.0f",windDir,getprop("environment/wind-speed-kt")));
},
is_false: NOTHING,
},
},
{
id:'windArrow',
impl: {
init: func(nd,symbol),
predicate: func(nd) (!(nd.in_mode('toggle_display_mode', ['PLAN']) and (nd.get_switch('toggle_display_type') == "LCD")) and nd.aircraft_source.get_spd() > 100),
is_true: func(nd) {
nd.symbols.windArrow.show();
var windArrowRot = getprop("environment/wind-from-heading-deg");
if((nd.in_mode('toggle_display_mode', ['MAP','PLAN']) and nd.get_switch('toggle_display_type') == "CRT")
or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD"))
windArrowRot = windArrowRot - nd.aircraft_source.get_trk_mag();
else
windArrowRot = windArrowRot - nd.aircraft_source.get_hdg_mag();
nd.symbols.windArrow.setRotation(windArrowRot*D2R);
},
is_false: func(nd) nd.symbols.windArrow.hide(),
},
},
], # end of vector with features
}, # end of Boeing style
#####
##
## add support for other aircraft/ND types and styles here (Airbus etc)
## or move to other files.
##
## see: http://wiki.flightgear.org/NavDisplay#Custom_ND_Styles
## and: http://wiki.flightgear.org/NavDisplay#Adding_new_features
}; # end of NDStyles

View file

@ -11,7 +11,7 @@ var draw_storm = func (group, storm, controller=nil, lod=0) {
var storm_grp = group.createChild("group","storm"); # one group for each storm
storm_grp.createChild("image")
.setFile("Nasal/canvas/map/storm.png")
.setFile("Nasal/canvas/map/Images/storm.png")
.setSize(128*radiusNm,128*radiusNm)
.setTranslation(-64*radiusNm,-64*radiusNm)
.setCenter(0,0);

View file

@ -8,18 +8,20 @@ StormModel.init = func {
foreach (var n; props.globals.getNode("/instrumentation/wxradar",1).getChildren("storm")) {
# Model 3 degree radar beam
var stormLat = n.getNode("latitude-deg").getValue();
stormLon = n.getNode("longitude-deg").getValue();
acLat = getprop("/position/latitude-deg");
acLon = getprop("/position/longitude-deg");
stormGeo = geo.Coord.new();
acGeo = geo.Coord.new();
var stormLon = n.getNode("longitude-deg").getValue();
# FIXME: once ported to MapStructure, these should use the encapsulated "aircraft source"/driver stuff
var acLat = getprop("/position/latitude-deg");
var acLon = getprop("/position/longitude-deg");
var stormGeo = geo.Coord.new();
var acGeo = geo.Coord.new();
stormGeo.set_latlon(stormLat, stormLon);
acGeo.set_latlon(acLat, acLon);
var directDistance = acGeo.direct_distance_to(stormGeo);
beamH = 0.1719 * directDistance; # M2FT * tan(3deg)
beamBase = getprop("position/altitude-ft") - beamH;
var beamH = 0.1719 * directDistance; # M2FT * tan(3deg)
var beamBase = getprop("position/altitude-ft") - beamH;
if (n.getNode("top-altitude-ft").getValue() > beamBase) {
me.push( { lat: stormLat, lon : stormLon, radiusNm : n.getNode("radius-nm").getValue() } );

View file

@ -67,22 +67,22 @@
# ANSI color code wrappers (see $ man console_codes)
#
var _title = func(s) globals.string.color("33;42;1", s); # backtrace header
var _section = func(s) globals.string.color("37;41;1", s); # backtrace frame
var _error = func(s) globals.string.color("31;1", s); # internal errors
var _bench = func(s) globals.string.color("37;45;1", s); # benchmark info
var _title = func(s, color=nil) globals.string.color("33;42;1", s, color); # backtrace header
var _section = func(s, color=nil) globals.string.color("37;41;1", s, color); # backtrace frame
var _error = func(s, color=nil) globals.string.color("31;1", s, color); # internal errors
var _bench = func(s, color=nil) globals.string.color("37;45;1", s); # benchmark info
var _nil = func(s) globals.string.color("32", s); # nil
var _string = func(s) globals.string.color("31", s); # "foo"
var _num = func(s) globals.string.color("31", s); # 0.0
var _bracket = func(s) globals.string.color("", s); # [ ]
var _brace = func(s) globals.string.color("", s); # { }
var _angle = func(s) globals.string.color("", s); # < >
var _vartype = func(s) globals.string.color("33", s); # func ghost
var _proptype = func(s) globals.string.color("34", s); # BOOL INT LONG DOUBLE ...
var _path = func(s) globals.string.color("36", s); # /some/property/path
var _internal = func(s) globals.string.color("35", s); # me parents
var _varname = func(s) s; # variable_name
var _nil = func(s, color=nil) globals.string.color("32", s, color); # nil
var _string = func(s, color=nil) globals.string.color("31", s, color); # "foo"
var _num = func(s, color=nil) globals.string.color("31", s, color); # 0.0
var _bracket = func(s, color=nil) globals.string.color("", s, color); # [ ]
var _brace = func(s, color=nil) globals.string.color("", s, color); # { }
var _angle = func(s, color=nil) globals.string.color("", s, color); # < >
var _vartype = func(s, color=nil) globals.string.color("33", s, color); # func ghost
var _proptype = func(s, color=nil) globals.string.color("34", s, color); # BOOL INT LONG DOUBLE ...
var _path = func(s, color=nil) globals.string.color("36", s, color); # /some/property/path
var _internal = func(s, color=nil) globals.string.color("35", s, color); # me parents
var _varname = func(s, color=nil) s; # variable_name
##
@ -142,7 +142,7 @@ var _tree = func(n, graph = 1, prefix = "", level = 0) {
}
var attributes = func(p, verbose = 1) {
var attributes = func(p, verbose = 1, color=nil) {
var r = p.getAttribute("readable") ? "" : "r";
var w = p.getAttribute("writable") ? "" : "w";
var R = p.getAttribute("trace-read") ? "R" : "";
@ -159,24 +159,24 @@ var attributes = func(p, verbose = 1) {
type ~= ", L" ~ l;
if (verbose and (var c = p.getAttribute("references")) > 2)
type ~= ", #" ~ (c - 2);
return _proptype(type ~ ")");
return _proptype(type ~ ")", color);
}
var _dump_prop = func(p) {
_path(p.getPath()) ~ " = " ~ debug.string(p.getValue()) ~ " " ~ attributes(p);
var _dump_prop = func(p, color=nil) {
_path(p.getPath(), color) ~ " = " ~ debug.string(p.getValue()) ~ " " ~ attributes(p);
}
var _dump_var = func(v) {
var _dump_var = func(v, color=nil) {
if (v == "me" or v == "parents")
return _internal(v);
return _internal(v, color);
else
return _varname(v);
return _varname(v, color);
}
var _dump_string = func(str) {
var _dump_string = func(str, color=nil) {
var s = "'";
for (var i = 0; i < size(str); i += 1) {
var c = str[i];
@ -193,55 +193,55 @@ var _dump_string = func(str) {
else
s ~= sprintf("\\x%02x", c);
}
return _string(s ~ "'");
return _string(s ~ "'", color);
}
# dump hash keys as variables if they are valid variable names, or as string otherwise
var _dump_key = func(s) {
var _dump_key = func(s, color=nil) {
if (num(s) != nil)
return _num(s);
return _num(s, color);
if (!size(s))
return _dump_string(s);
return _dump_string(s, color);
if (!globals.string.isalpha(s[0]) and s[0] != `_`)
return _dump_string(s);
return _dump_string(s, color);
for (var i = 1; i < size(s); i += 1)
if (!globals.string.isalnum(s[i]) and s[i] != `_`)
return _dump_string(s);
_dump_var(s);
return _dump_string(s, color);
_dump_var(s, color);
}
var string = func(o) {
var string = func(o, color=nil) {
var t = typeof(o);
if (t == "nil") {
return _nil("nil");
return _nil("nil", color);
} elsif (t == "scalar") {
return num(o) == nil ? _dump_string(o) : _num(o~"");
return num(o) == nil ? _dump_string(o, color) : _num(o~"", color);
} elsif (t == "vector") {
var s = "";
forindex (var i; o)
s ~= (i == 0 ? "" : ", ") ~ debug.string(o[i]);
return _bracket("[") ~ s ~ _bracket("]");
s ~= (i == 0 ? "" : ", ") ~ debug.string(o[i], color);
return _bracket("[", color) ~ s ~ _bracket("]", color);
} elsif (t == "hash") {
if (contains(o, "parents") and typeof(o.parents) == "vector"
and size(o.parents) == 1 and o.parents[0] == props.Node)
return _angle("<") ~ _dump_prop(o) ~ _angle(">");
return _angle("<", color) ~ _dump_prop(o, color) ~ _angle(">", color);
var k = keys(o);
var s = "";
forindex (var i; k)
s ~= (i == 0 ? "" : ", ") ~ _dump_key(k[i]) ~ ": " ~ debug.string(o[k[i]]);
return _brace("{") ~ " " ~ s ~ " " ~ _brace("}");
s ~= (i == 0 ? "" : ", ") ~ _dump_key(k[i], color) ~ ": " ~ debug.string(o[k[i]], color);
return _brace("{", color) ~ " " ~ s ~ " " ~ _brace("}", color);
} elsif (t == "ghost") {
return _angle("<") ~ _nil(ghosttype(o)) ~ _angle(">");
return _angle("<", color) ~ _nil(ghosttype(o), color) ~ _angle(">", color);
} else {
return _angle("<") ~ _vartype(t) ~ _angle(">");
return _angle("<", color) ~ _vartype(t, color) ~ _angle(">", color);
}
}

View file

@ -1,230 +0,0 @@
# failures.nas a manager for failing systems based on MTBF/MCBF
# Time between MTBF checks
var dt = 10;
# Root property for failure information
var failure_root = "/sim/failure-manager";
# Enumerations
var type = { MTBF : 1, MCBF: 2 };
var fail = { SERVICEABLE : 1, JAM : 2, ENGINE: 3};
# This hash contains a mapping from property entry to a failure object
# containing the following members:
# type: MTBF|MCBF Mean Time Between Failures/Mean Cycle Between Failures
# desc: <description> Description of property for screen output
# failure: SERVICEABLE Property has a "serviceable" child that can be set to false
# failure: JAM Property is failed by marking as Read-Only
# failure: ENGINE Special case for engines, where a variety of properties are set.
# failure: <prop> Property is failed by setting another property to false
var breakHash = {
"/instrumentation/adf" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "ADF" },
"/instrumentation/dme" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "DME" },
"/instrumentation/airspeed-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "ASI" },
"/instrumentation/altimeter" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Altimeter" },
"/instrumentation/attitude-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Attitude Indicator" },
"/instrumentation/heading-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Heading Indicator" },
"/instrumentation/magnetic-compass" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Magnetic Compass" },
"/instrumentation/nav[0]/gs" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 1 Glideslope" },
"/instrumentation/nav[0]/cdi" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 1 CDI" },
"/instrumentation/nav[1]/gs" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 2 Glideslope" },
"/instrumentation/nav[1]/cdi" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 2 CDI" },
"/instrumentation/slip-skid-ball" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Slip/Skid Ball" },
"/instrumentation/turn-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Turn Indicator" },
"/instrumentation/vertical-speed-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "VSI" },
"/systems/electrical" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Electrical system" },
"/systems/pitot" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Pitot system" },
"/systems/static" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Static system" },
"/systems/vacuum" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Vacuum system" },
"/controls/gear/gear-down" : { type: type.MCBF, failure: "/gear/serviceable", desc: "Gear" },
"/controls/flight/aileron" : { type: type.MTBF, failure: fail.JAM, desc: "Aileron" },
"/controls/flight/elevator" : { type: type.MTBF, failure: fail.JAM, desc: "Elevator" },
"/controls/flight/rudder" : { type: type.MTBF, failure: fail.JAM, desc: "Rudder" },
"/controls/flight/flaps" : { type: type.MCBF, failure: fail.JAM, desc: "Flaps" },
"/controls/flight/speedbrake" : { type: type.MCBF, failure: fail.JAM, desc: "Speed Brake" }
};
# Return the failure entry for a given property
var getFailure = func (prop) {
var o = breakHash[prop];
if (o.failure == fail.SERVICEABLE) {
return prop ~ "/serviceable";
} elsif (o.failure == fail.ENGINE) {
return failure_root ~ prop ~ "/serviceable";
} elsif (o.failure == fail.JAM) {
return failure_root ~ prop ~ "/serviceable";
} else {
return o.failure;
}
}
# Fail a given property, either using a serviceable flag, or by jamming the property
var failProp = func(prop) {
var o = breakHash[prop];
var p = getFailure(prop);
if (getprop(p) == 1) {
setprop(p, 0);
# We always print to the console
print(getprop("/sim/time/gmt-string") ~ " : " ~ o.desc ~ " failed");
if (getprop(failure_root ~ "/display-on-screen")) {
# Display message to the screen in red
screen.log.write(o.desc ~ " failed", 1.0, 0.0, 0.0);
}
}
}
# Unfail a given property, used for resetting a failure state.
var unfailProp = func(prop)
{
var p = getFailure(prop);
setprop(p, 1);
}
# Unfail all the failed properties
var unfail = func {
foreach(var prop; keys(breakHash)) {
unfailProp(prop);
}
}
# Listener to jam a property. Note that the property to jam is
# encoded within the property name
var jamListener = func(p) {
var jamprop = string.replace(p.getParent().getPath(), failure_root, "");
#jamprop = string.replace(jamprop, "/serviceable", "");
var prop = props.globals.getNode(jamprop);
if (p.getValue()) {
prop.setAttribute("writable", 1);
} else {
prop.setAttribute("writable", 0);
}
}
# Listener for an engine property. Note that the engine to set is
# encoded within the property name. We set both the magnetos and
# cutoff to handle different engine models.
var engineListener = func(p) {
var e = string.replace(p.getParent().getPath(), failure_root, "");
var prop = props.globals.getNode(e);
if (p.getValue()) {
# Enable the properties, but don't set the magnetos, as they may
# be off for a reason.
var magnetos = props.globals.getNode("/controls/" ~ e ~ "/magnetos", 1);
var cutoff = props.globals.getNode("/controls/" ~ e ~ "/cutoff", 1);
magnetos.setAttribute("writable", 1);
cutoff.setAttribute("writable", 1);
cutoff.setValue(0);
} else {
# Switch off the engine, and disable writing to it.
var magnetos = props.globals.getNode("/controls/" ~ e ~ "/magnetos", 1);
var cutoff = props.globals.getNode("/controls/" ~ e ~ "/cutoff", 1);
magnetos.setValue(0);
cutoff.setValue(1);
magnetos.setAttribute("writable", 0);
cutoff.setAttribute("writable", 0);
}
}
# Perform a MCBF check against a failure property.
var checkMCBF = func(prop) {
var mcbf = getprop(failure_root ~ prop.getPath() ~ "/mcbf");
# mcbf == mean cycles between failures
# hence 2*mcbf is the number of _half-cycles_ between failures,
# which is relevant because we do this check on each half-cycle:
if ((mcbf > 0) and !int(2 * mcbf * rand())) {
# Get the property information.
failProp(prop.getPath());
}
}
# Timer based loop to check MTBF properties
var checkMTBF = func {
foreach(var prop; keys(breakHash)) {
var o = breakHash[prop];
if (o.type == type.MTBF) {
var mtbf = getprop(failure_root ~ prop ~ "/mtbf");
if (mtbf and !int(rand() * mtbf / dt)) {
failProp(prop);
}
}
}
settimer(checkMTBF, dt);
}
# Function to set all MTBF failures to a give value. Mainly for testing.
var setAllMTBF = func(mtbf) {
foreach(var prop; keys(breakHash)) {
var o = breakHash[prop];
if (o.type == type.MTBF) {
setprop(failure_root ~ prop ~ "/mtbf", mtbf);
}
}
}
# Function to set all MCBF failures to a give value. Mainly for testing.
var setAllMCBF = func(mcbf) {
foreach(var prop; keys(breakHash)) {
var o = breakHash[prop];
if (o.type == type.MCBF) {
setprop(failure_root ~ prop ~ "/mcbf", mcbf);
}
}
}
# Initialization, called once Nasal and the FDM are loaded properly.
var fdm_init_listener = _setlistener("/sim/signals/fdm-initialized", func {
removelistener(fdm_init_listener); # uninstall, so we're only called once
srand();
# Engines are added dynamically because there may be an arbitrary number
var i = 1;
foreach (var e; props.globals.getNode("/engines").getChildren("engine")) {
breakHash[e.getPath()] = { type: type.MTBF, failure: fail.ENGINE, desc : "Engine " ~ i };
i = i+1;
}
# Set up serviceable, MCBF and MTBF properties.
foreach(var prop; keys(breakHash)) {
var o = breakHash[prop];
var t = "/mcbf";
if (o.type == type.MTBF) {
t = "/mtbf";
}
# Set up the MTBF/MCFB properties to 0. Note that they are in a separate
# subtree, as there's no guarantee that the property isn't a leaf.
props.globals.initNode(failure_root ~ prop ~ t, 0);
if (o.failure == fail.SERVICEABLE) {
# If the property has a serviceable property, set it if appropriate.
props.globals.initNode(prop ~ "/serviceable", 1, "BOOL");
} elsif (o.failure == fail.JAM) {
# In the JAM case, we actually have a dummy serviceable property for the GUI.
props.globals.initNode(failure_root ~ prop ~ "/serviceable", 1, "BOOL");
setlistener(failure_root ~ prop ~ "/serviceable", jamListener);
} elsif (o.failure == fail.ENGINE) {
# In the JAM case, we actually have a dummy serviceable property for the GUI.
props.globals.initNode(failure_root ~ prop ~ "/serviceable", 1, "BOOL");
setlistener(failure_root ~ prop ~ "/serviceable", engineListener);
} else {
# If the serviceable property is actually defined, check it is set.
props.globals.initNode(o.failure, 1, "BOOL");
}
if (o.type == type.MCBF) {
# Set up listener for MCBF properties, only when the value changes.
setlistener(prop, checkMCBF, 0, 0);
}
}
# Start checking for failures.
checkMTBF();
});

View file

@ -27,6 +27,15 @@ var L2GAL = 1 / GAL2L;
# container for local variables, so as not to clutter the global namespace
var __ = {};
##
# Aborts execution if <condition> evaluates to false.
# Prints an optional message if present, or just "assertion failed!"
#
var assert = func (condition, message=nil) {
message != nil or (message = "assertion failed!");
condition or die(message);
}
##
# Returns true if the first object is an instance of the second
# (class) object. Example: isa(someObject, props.Node)
@ -180,4 +189,3 @@ settimer(func {
if(size(file) > 4 and substr(file, -4) == ".nas")
io.load_nasal(path ~ "/" ~ file, substr(file, 0, size(file) - 4));
}, 0);

View file

@ -1381,11 +1381,6 @@ _setlistener("/sim/signals/fdm-initialized", func {
do_welcome = 0;
});
# load ATC chatter module on demand
setprop("/nasal/atc-chatter/enabled", getprop("/sim/sound/chatter/enabled"));
_setlistener("/sim/sound/chatter/enabled", func {
setprop("/nasal/atc-chatter/enabled", getprop("/sim/sound/chatter/enabled"));
});
##
# overwrite custom shader settings when quality-level is set on startup

View file

@ -47,8 +47,6 @@ var include = func(file) {
if (contains(ns, module))
return;
ns[module] = "included";
var code = call(compile, [readfile(path), path], var err = []);
if (size(err)) {
if (find("Parse error:", err[0]) < 0)
@ -57,6 +55,7 @@ var include = func(file) {
die(sprintf("%s\n in included file: %s", err[0], path));
}
ns[module] = "included";
call(bind(code, ns, fn), [], nil, ns);
}
@ -425,4 +424,3 @@ _setlistener("/sim/signals/nasal-dir-initialized", func {
}
})();
});

View file

@ -227,6 +227,11 @@ var wrapNode = func(node) { { parents : [Node], _g : node } }
#
var globals = wrapNode(_globals());
##
# Shortcut for props.globals.getNode().
#
var getNode = func return call(props.globals.getNode, arg, props.globals);
##
# Sets all indexed property children to a single value. arg[0]
# specifies a property name (e.g. /controls/engines/engine), arg[1] a

View file

@ -15,7 +15,7 @@ var RouteManagerDelegate = {
departureChanged: func
{
debug.dump('saw departure changed');
printlog('info', 'saw departure changed');
me.flightplan.clearWPType('sid');
if (me.flightplan.departure == nil)
return;
@ -36,13 +36,13 @@ var RouteManagerDelegate = {
# and we have a SID
var sid = me.flightplan.sid;
debug.dump('routing via SID ' ~ sid.id);
printlog('info', 'routing via SID ' ~ sid.id);
me.flightplan.insertWaypoints(sid.route(me.flightplan.departure_runway), 1);
},
arrivalChanged: func
{
debug.dump('saw arrival changed');
printlog('info', 'saw arrival changed');
me.flightplan.clearWPType('star');
me.flightplan.clearWPType('approach');
if (me.flightplan.destination == nil)
@ -57,17 +57,17 @@ var RouteManagerDelegate = {
}
if (me.flightplan.star != nil) {
debug.dump('routing via STAR ' ~ me.flightplan.star.id);
printlog('info', 'routing via STAR ' ~ me.flightplan.star.id);
var wps = me.flightplan.star.route(me.flightplan.destination_runway);
me.flightplan.insertWaypoints(wps, -1);
}
if (me.flightplan.approach != nil) {
debug.dump('routing via approach ' ~ me.flightplan.approach.id);
printlog('info', 'routing via approach ' ~ me.flightplan.approach.id);
var wps = me.flightplan.approach.route();
me.flightplan.insertWaypoints(wps, -1);
} else {
debug.dump('routing direct to runway ' ~ me.flightplan.destination_runway.id);
printlog('info', 'routing direct to runway ' ~ me.flightplan.destination_runway.id);
# no approach, just use the runway waypoint
var wp = createWPFrom(me.flightplan.destination_runway);
wp.wp_role = 'approach';
@ -77,14 +77,14 @@ var RouteManagerDelegate = {
cleared: func
{
debug.dump("saw active flightplan cleared, deactivating");
printlog('info', "saw active flightplan cleared, deactivating");
# see http://https://code.google.com/p/flightgear-bugs/issues/detail?id=885
fgcommand("activate-flightplan", props.Node.new({"activate": 0}));
},
endOfFlightPlan: func
{
debug.dump("end of flight-plan, deactivating");
printlog('info', "end of flight-plan, deactivating");
fgcommand("activate-flightplan", props.Node.new({"activate": 0}));
}
};
@ -105,7 +105,7 @@ var FMSDelegate = {
var wow = getprop('gear/gear[0]/wow');
var gs = getprop('velocities/groundspeed-kt');
if (wow and (gs < 25)) {
debug.dump('touchdown on destination runway, end of route.');
printlog('info', 'touchdown on destination runway, end of route.');
me.landingCheck.stop();
# record touch-down time?
me.flightplan.finish();
@ -118,7 +118,7 @@ var FMSDelegate = {
endOfFlightPlan: func
{
debug.dump('end of flight-plan');
printlog('info', 'end of flight-plan');
},
currentWaypointChanged: func
@ -128,12 +128,12 @@ var FMSDelegate = {
me.landingCheck = nil; # delete timer
}
#debug.dump('saw current WP changed, now ' ~ me.flightplan.current);
#printlog('info', 'saw current WP changed, now ' ~ me.flightplan.current);
var active = me.flightplan.currentWP();
if (active == nil) return;
if (active.alt_cstr_type == "at") {
debug.dump('new WP has valid altitude restriction, setting on AP');
printlog('info', 'new WP has valid altitude restriction, setting on AP');
setprop('/autopilot/settings/target-altitude-ft', active.alt_cstr);
}

View file

@ -486,10 +486,11 @@ var scanf = func(test, format, result) {
# ANSI colors (see $ man console_codes)
#
var setcolors = func(enabled) {
if (enabled and getprop("/sim/startup/stderr-to-terminal"))
color = func(color, s) { "\x1b[" ~ color ~ "m" ~ s ~ "\x1b[m" }
else
color = func(dummy, s) { s }
color_enabled = (enabled and getprop("/sim/startup/stderr-to-terminal"));
}
var color = func(color, s, enabled=nil) {
if (enabled == nil) enabled = color_enabled;
return enabled ? "\x1b[" ~ color ~ "m" ~ s ~ "\x1b[m" : s;
}
@ -499,10 +500,9 @@ var setcolors = func(enabled) {
#
# print(string.color("31;1", "this is red"));
#
var color = func nil;
setcolors(getprop("/sim/startup/terminal-ansi-colors"));
var color_enabled = 0;
_setlistener("/sim/signals/nasal-dir-initialized", func {
setlistener("/sim/startup/terminal-ansi-colors", func(n) setcolors(n.getBoolValue()));
setlistener("/sim/startup/terminal-ansi-colors", func(n) setcolors(n.getBoolValue()), 1, 0);
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Some files were not shown because too many files have changed in this diff Show more