Rewrite with use Object Oriented Programming
This commit is contained in:
parent
92fd83615d
commit
3c306ca039
@ -23,7 +23,6 @@ var unload = func(addon) {
|
|||||||
# e.g. myCanvas.del();
|
# e.g. myCanvas.del();
|
||||||
|
|
||||||
aerotow.uninit();
|
aerotow.uninit();
|
||||||
thermal.uninit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var main = func(addon) {
|
var main = func(addon) {
|
||||||
@ -40,18 +39,29 @@ var main = func(addon) {
|
|||||||
|
|
||||||
loadExtraNasalFiles(addon);
|
loadExtraNasalFiles(addon);
|
||||||
|
|
||||||
aerotow.init();
|
aerotow.init(addon);
|
||||||
thermal.init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Load extra Nasal files in main add-on directory
|
# Load extra Nasal files in main add-on directory
|
||||||
#
|
#
|
||||||
|
# addon - Addob object
|
||||||
|
#
|
||||||
var loadExtraNasalFiles = func (addon) {
|
var loadExtraNasalFiles = func (addon) {
|
||||||
foreach (var scriptName; ["aerotow", "messages", "thermal"]) {
|
var modules = [
|
||||||
|
"nasal/aircraft",
|
||||||
|
"nasal/message",
|
||||||
|
"nasal/dialogs/route",
|
||||||
|
"nasal/dialogs/thermal",
|
||||||
|
"nasal/flight-plan",
|
||||||
|
"nasal/scenario",
|
||||||
|
"aerotow",
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (var scriptName; modules) {
|
||||||
var fileName = addon.basePath ~ "/" ~ scriptName ~ ".nas";
|
var fileName = addon.basePath ~ "/" ~ scriptName ~ ".nas";
|
||||||
|
|
||||||
if (io.load_nasal(fileName, scriptName)) {
|
if (io.load_nasal(fileName, "aerotow")) {
|
||||||
print("Aerotown Add-on module \"", scriptName, "\" loaded OK");
|
print("Aerotown Add-on module \"", scriptName, "\" loaded OK");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@
|
|||||||
<script><![CDATA[
|
<script><![CDATA[
|
||||||
# Run stopAerotow() with a delay to ensure that the engine sound turns off
|
# Run stopAerotow() with a delay to ensure that the engine sound turns off
|
||||||
var timer = maketimer(1, func () {
|
var timer = maketimer(1, func () {
|
||||||
aerotow.stopAerotow();
|
aerotow.g_Aerotow.stopAerotow();
|
||||||
});
|
});
|
||||||
timer.singleShot = 1;
|
timer.singleShot = 1;
|
||||||
timer.start();
|
timer.start();
|
||||||
|
804
aerotow.nas
804
aerotow.nas
@ -10,721 +10,123 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
#
|
#
|
||||||
# Constants
|
# Aerotow object
|
||||||
#
|
#
|
||||||
var ADDON = addons.getAddon("org.flightgear.addons.Aerotow");
|
var Aerotow = {
|
||||||
var ADDON_NODE_PATH = ADDON.node.getPath();
|
#
|
||||||
var FILENAME_SCENARIO = "aerotown-addon.xml";
|
# Constructor
|
||||||
var FILENAME_FLIGHTPLAN = "aerotown-addon-flightplan.xml";
|
#
|
||||||
var PATH_SCENARIO = ADDON.storagePath ~ "/" ~ FILENAME_SCENARIO;
|
# addon - Addon object
|
||||||
var PATH_FLIGHTPLAN = ADDON.storagePath ~ "/AI/FlightPlans/" ~ FILENAME_FLIGHTPLAN;
|
#
|
||||||
var SCENARIO_ID = "aerotow_addon";
|
new: func (addon) {
|
||||||
var SCENARIO_NAME = "Aerotow Add-on";
|
var obj = { parents: [Aerotow] };
|
||||||
var SCENARIO_DESC = "This scenario starts the towing plane at the airport where the pilot with the glider is located. Use Ctrl-o to hook the plane.";
|
|
||||||
var MAX_ROUTE_WAYPOINT = 10;
|
|
||||||
var DISTANCE_DETERMINANT = 1000; # meters
|
|
||||||
|
|
||||||
#
|
obj.addon = addon;
|
||||||
# Global variables
|
obj.addonNodePath = addon.node.getPath();
|
||||||
#
|
obj.listeners = [];
|
||||||
var g_wptCount = 0;
|
|
||||||
var g_isScenarioLoaded = 0;
|
|
||||||
var g_fpFileHandler = nil; # Handler for wrire flight plan to file
|
|
||||||
var g_coord = nil; # Coordinates for flight plan
|
|
||||||
var g_heading = nil; # AI plane heading
|
|
||||||
var g_altitude = nil; # AI plane altitude
|
|
||||||
var g_towListeners = [];
|
|
||||||
|
|
||||||
#
|
obj.message = Message.new();
|
||||||
# Initialize aerotow module
|
obj.thermal = Thermal.new(addon, obj.message);
|
||||||
#
|
obj.scenario = Scenario.new(addon, obj.message);
|
||||||
var init = func () {
|
|
||||||
# Listener for ai-model property triggered when the user select a tow aircraft from add-on menu
|
|
||||||
append(g_towListeners, setlistener(ADDON_NODE_PATH ~ "/addon-devel/ai-model", func () {
|
|
||||||
restartAerotow();
|
|
||||||
}));
|
|
||||||
|
|
||||||
append(g_towListeners, setlistener("/sim/presets/longitude-deg", func () {
|
# Listener for ai-model property triggered when the user select a tow aircraft from add-on menu
|
||||||
# User change airport/runway
|
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/ai-model", func () {
|
||||||
initialFlightPlan();
|
obj.restartAerotow();
|
||||||
}));
|
|
||||||
|
|
||||||
initialFlightPlan();
|
|
||||||
|
|
||||||
# Set listener for aerotow combo box value in route dialog for recalculate altitude change
|
|
||||||
append(g_towListeners, setlistener(ADDON_NODE_PATH ~ "/addon-devel/route/ai-model", func () {
|
|
||||||
calculateAltChangeAndTotals();
|
|
||||||
}));
|
|
||||||
|
|
||||||
# Set listeners for distance fields for calculate altitude change
|
|
||||||
for (var i = 0; i < MAX_ROUTE_WAYPOINT; i = i + 1) {
|
|
||||||
append(g_towListeners, setlistener(ADDON_NODE_PATH ~ "/addon-devel/route/wpt[" ~ i ~ "]/distance-m", func () {
|
|
||||||
calculateAltChangeAndTotals();
|
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
return obj;
|
||||||
# Calculate total distance and altitude and put in to property tree
|
},
|
||||||
#
|
|
||||||
var calculateAltChangeAndTotals = func () {
|
|
||||||
var totalDistance = 0.0;
|
|
||||||
var totalAlt = 0.0;
|
|
||||||
var isEnd = 0;
|
|
||||||
|
|
||||||
var isRouteMode = 1;
|
#
|
||||||
var perf = getAircraftPerformance(isRouteMode);
|
# Uninitialize aerotow module
|
||||||
|
#
|
||||||
|
del: func () {
|
||||||
|
me.thermal.del();
|
||||||
|
|
||||||
for (var i = 0; i < MAX_ROUTE_WAYPOINT; i = i + 1) {
|
foreach (var listener; me.listeners) {
|
||||||
var distance = getprop(ADDON_NODE_PATH ~ "/addon-devel/route/wpt[" ~ i ~ "]/distance-m");
|
removelistener(listener);
|
||||||
var altChange = getAltChange(perf.vs, distance);
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/wpt[" ~ i ~ "]/alt-change-agl-ft", altChange);
|
|
||||||
|
|
||||||
if (!isEnd) {
|
|
||||||
if (distance > 0.0) {
|
|
||||||
totalDistance = totalDistance + distance;
|
|
||||||
totalAlt = totalAlt + altChange;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
isEnd = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/total/distance", totalDistance);
|
#
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/total/alt", totalAlt);
|
# Function for restart AI scenario with delay when the sound has to stop.
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
restartAerotow: func () {
|
||||||
|
me.message.success("Aerotow in the way");
|
||||||
|
|
||||||
|
# Stop playing engine sound
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/sound/enable", 0);
|
||||||
|
|
||||||
|
# Wait a second for the engine sound to turn off
|
||||||
|
var timer = maketimer(1, func () {
|
||||||
|
me.unloadScenario();
|
||||||
|
});
|
||||||
|
timer.singleShot = 1;
|
||||||
|
timer.start();
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unload scenario and start a new one
|
||||||
|
#
|
||||||
|
unloadScenario: func () {
|
||||||
|
if (!me.scenario.unload()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start aerotow with delay to avoid duplicate engine sound playing
|
||||||
|
var timer = maketimer(1, func () {
|
||||||
|
me.startAerotow();
|
||||||
|
});
|
||||||
|
timer.singleShot = 1;
|
||||||
|
timer.start();
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Main function to prepare AI scenario and run it.
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
startAerotow: func () {
|
||||||
|
if (!me.scenario.unload()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!me.scenario.generateXml()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return me.scenario.load();
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Function for unload our AI scenario.
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
stopAerotow: func () {
|
||||||
|
var withMessages = 1;
|
||||||
|
return me.scenario.unload(withMessages);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var g_Aerotow = nil;
|
||||||
|
|
||||||
|
#
|
||||||
|
# Initialize Aerotow
|
||||||
|
#
|
||||||
|
# addon - Addon object
|
||||||
|
#
|
||||||
|
var init = func (addon) {
|
||||||
|
g_Aerotow = Aerotow.new(addon);
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# Uninitialize aerotow module
|
# Uninitialize Aerotow
|
||||||
#
|
#
|
||||||
var uninit = func () {
|
var uninit = func () {
|
||||||
foreach (var listener; g_towListeners) {
|
if (g_Aerotow) {
|
||||||
removelistener(listener);
|
g_Aerotow.del();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
|
||||||
# Function for restart AI scenario with delay when the sound has to stop.
|
|
||||||
#
|
|
||||||
# Return 1 on successful, otherwise 0.
|
|
||||||
#
|
|
||||||
var restartAerotow = func () {
|
|
||||||
messages.displayOk("Aerotow in the way");
|
|
||||||
|
|
||||||
# Stop playing engine sound
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/sound/enable", 0);
|
|
||||||
|
|
||||||
# Wait a second for the engine sound to turn off
|
|
||||||
var timer = maketimer(1, func () {
|
|
||||||
unloadScenario();
|
|
||||||
});
|
|
||||||
timer.singleShot = 1;
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Unload scenario and start a new one
|
|
||||||
#
|
|
||||||
var unloadScenario = func () {
|
|
||||||
if (g_isScenarioLoaded) {
|
|
||||||
var args = props.Node.new({ "name": SCENARIO_ID });
|
|
||||||
if (fgcommand("unload-scenario", args)) {
|
|
||||||
g_isScenarioLoaded = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Start aerotow with delay to avoid duplicate engine sound playing
|
|
||||||
var timer = maketimer(1, func () {
|
|
||||||
startAerotow();
|
|
||||||
});
|
|
||||||
timer.singleShot = 1;
|
|
||||||
timer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Main function to prepare AI scenario and run it.
|
|
||||||
#
|
|
||||||
# Return 1 on successful, otherwise 0.
|
|
||||||
#
|
|
||||||
var startAerotow = func () {
|
|
||||||
var args = props.Node.new({ "name": SCENARIO_ID });
|
|
||||||
|
|
||||||
if (g_isScenarioLoaded) {
|
|
||||||
if (fgcommand("unload-scenario", args)) {
|
|
||||||
g_isScenarioLoaded = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateScenarioXml();
|
|
||||||
|
|
||||||
if (!generateFlightPlanXml()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fgcommand("load-scenario", args)) {
|
|
||||||
g_isScenarioLoaded = 1;
|
|
||||||
messages.displayOk("Let's fly!");
|
|
||||||
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/sound/enable", 1);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.displayError("Tow failed!");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Function for unload our AI scenario.
|
|
||||||
#
|
|
||||||
# Return 1 on successful, otherwise 0.
|
|
||||||
#
|
|
||||||
var stopAerotow = func () {
|
|
||||||
if (g_isScenarioLoaded) {
|
|
||||||
var args = props.Node.new({ "name": SCENARIO_ID });
|
|
||||||
if (fgcommand("unload-scenario", args)) {
|
|
||||||
g_isScenarioLoaded = 0;
|
|
||||||
|
|
||||||
messages.displayOk("Aerotown disabled");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.displayError("Aerotown disable failed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.displayOk("Aerotown already disabled");
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Generate the XML file with the AI scenario.
|
|
||||||
# The file will be stored to $FG_HOME/Export/aerotown-addon.xml.
|
|
||||||
#
|
|
||||||
var generateScenarioXml = func () {
|
|
||||||
var scenarioXml = {
|
|
||||||
"PropertyList": {
|
|
||||||
"scenario": {
|
|
||||||
"name": SCENARIO_NAME,
|
|
||||||
"description": SCENARIO_DESC,
|
|
||||||
"entry": {
|
|
||||||
"callsign": "FG-TOW",
|
|
||||||
"type": "aircraft",
|
|
||||||
"class": "aerotow-dragger",
|
|
||||||
"model": "Aircraft/Aerotow/Cub/Models/Cub-ai.xml", # default Cub
|
|
||||||
"flightplan": FILENAME_FLIGHTPLAN,
|
|
||||||
"repeat": 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var aiModel = getSelectedAircraft();
|
|
||||||
if (aiModel == "DR400") {
|
|
||||||
scenarioXml.PropertyList.scenario.entry.model = "Aircraft/Aerotow/DR400/Models/dr400-ai.xml";
|
|
||||||
}
|
|
||||||
else if (aiModel == "c182") {
|
|
||||||
scenarioXml.PropertyList.scenario.entry.model = "Aircraft/Aerotow/c182/Models/c182-ai.xml";
|
|
||||||
}
|
|
||||||
|
|
||||||
var node = props.Node.new(scenarioXml);
|
|
||||||
io.writexml(PATH_SCENARIO, node);
|
|
||||||
|
|
||||||
addScenarioToPropertyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Add our new scenario to the "/sim/ai/scenarios" property list
|
|
||||||
# so that FlightGear will be able to load it by "load-scenario" command.
|
|
||||||
#
|
|
||||||
var addScenarioToPropertyList = func () {
|
|
||||||
if (!isScenarioAdded()) {
|
|
||||||
var scenarioData = {
|
|
||||||
"name": SCENARIO_NAME,
|
|
||||||
"id": SCENARIO_ID,
|
|
||||||
"description": SCENARIO_DESC,
|
|
||||||
"path": PATH_SCENARIO,
|
|
||||||
};
|
|
||||||
|
|
||||||
props.globals.getNode("/sim/ai/scenarios").addChild("scenario").setValues(scenarioData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Return 1 if scenario is already added to "/sim/ai/scenarios" property list, otherwise return 0.
|
|
||||||
#
|
|
||||||
var isScenarioAdded = func () {
|
|
||||||
foreach (var scenario; props.globals.getNode("/sim/ai/scenarios").getChildren("scenario")) {
|
|
||||||
var id = scenario.getChild("id");
|
|
||||||
if (id != nil and id.getValue() == SCENARIO_ID) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Return name of selected aircraft. Possible values depend of isRouteMode: "Cub", "DR400", "c182".
|
|
||||||
#
|
|
||||||
# isRouteMode - use 1 to get the plane for the "Aerotow Route" dialog, use 0 (default) for call the airplane for towing
|
|
||||||
#
|
|
||||||
var getSelectedAircraft = func (isRouteMode = 0) {
|
|
||||||
if (isRouteMode) {
|
|
||||||
return getprop(ADDON_NODE_PATH ~ "/addon-devel/route/ai-model") or "Piper J3 Cub";
|
|
||||||
}
|
|
||||||
|
|
||||||
return getprop(ADDON_NODE_PATH ~ "/addon-devel/ai-model") or "Cub";
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Get airport an runway hash where the glider is located.
|
|
||||||
#
|
|
||||||
# Return hash with "airport" and "runway", otherwise nil.
|
|
||||||
#
|
|
||||||
var getAirportAndRunway = func () {
|
|
||||||
var icao = getprop("/sim/airport/closest-airport-id");
|
|
||||||
if (icao == nil) {
|
|
||||||
messages.displayError("Airport code cannot be obtained.");
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
var runwayName = getprop("/sim/atc/runway");
|
|
||||||
if (runwayName == nil) {
|
|
||||||
messages.displayError("Runway name cannot be obtained.");
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
var airport = airportinfo(icao);
|
|
||||||
|
|
||||||
if (!contains(airport.runways, runwayName)) {
|
|
||||||
messages.displayError("The " ~ icao ~" airport does not have runway " ~ runwayName);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
var runway = airport.runways[runwayName];
|
|
||||||
|
|
||||||
var minRwyLength = getMinRunwayLength();
|
|
||||||
if (runway.length < minRwyLength) {
|
|
||||||
messages.displayError(
|
|
||||||
"This runway is too short. Please choose a longer one than " ~ minRwyLength ~ " m "
|
|
||||||
~ "(" ~ math.round(minRwyLength * globals.M2FT) ~ " ft)."
|
|
||||||
);
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"airport": airport,
|
|
||||||
"runway": runway,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Initialize flight plan and set it to property tree
|
|
||||||
#
|
|
||||||
# Return 1 on successful, otherwise 0.
|
|
||||||
#
|
|
||||||
var initialFlightPlan = func () {
|
|
||||||
var location = getAirportAndRunway();
|
|
||||||
if (location == nil) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var perf = getAircraftPerformance();
|
|
||||||
|
|
||||||
initAircraftVariable(location.airport, location.runway, 0);
|
|
||||||
|
|
||||||
# inittial readonly waypoint
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/init-wpt/heading-change", g_heading);
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/init-wpt/distance-m", 100);
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/init-wpt/alt-change-agl-ft", perf.vs / 10);
|
|
||||||
|
|
||||||
# in air
|
|
||||||
var wptData = [
|
|
||||||
{"hdgChange": 0, "dist": 5000, "altChange": perf.vs * 5},
|
|
||||||
{"hdgChange": -90, "dist": 1000, "altChange": perf.vs},
|
|
||||||
{"hdgChange": -90, "dist": 1000, "altChange": perf.vs},
|
|
||||||
{"hdgChange": 0, "dist": 5000, "altChange": perf.vs * 5},
|
|
||||||
{"hdgChange": -90, "dist": 1500, "altChange": perf.vs * 1.5},
|
|
||||||
{"hdgChange": -90, "dist": 1000, "altChange": perf.vs},
|
|
||||||
{"hdgChange": 0, "dist": 5000, "altChange": perf.vs * 5},
|
|
||||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
|
||||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
|
||||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
|
||||||
];
|
|
||||||
|
|
||||||
# Default route
|
|
||||||
# ^ - airport with heading direction to north
|
|
||||||
# 1 - 1st waypoint
|
|
||||||
# 2 - 2nd waypoint, etc.
|
|
||||||
#
|
|
||||||
# 2 . . 1 7
|
|
||||||
# . . .
|
|
||||||
# . . .
|
|
||||||
# 3 . .
|
|
||||||
# . . .
|
|
||||||
# . . .
|
|
||||||
# . . .
|
|
||||||
# . . .
|
|
||||||
# . . .
|
|
||||||
# . . .
|
|
||||||
# . ^ 6
|
|
||||||
# . .
|
|
||||||
# . .
|
|
||||||
# 4 . . . . 5
|
|
||||||
|
|
||||||
var index = 0;
|
|
||||||
foreach (var wpt; wptData) {
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/wpt[" ~ index ~ "]/heading-change", wpt.hdgChange);
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/wpt[" ~ index ~ "]/distance-m", wpt.dist);
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/route/wpt[" ~ index ~ "]/alt-change-agl-ft", wpt.altChange);
|
|
||||||
|
|
||||||
index = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateAltChangeAndTotals();
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Generate the XML file with the flight plane for our plane for AI scenario.
|
|
||||||
# The file will be stored to $FG_HOME/Export/aerotown-addon-flightplan.xml.
|
|
||||||
#
|
|
||||||
# Return 1 on successful, otherwise 0.
|
|
||||||
#
|
|
||||||
var generateFlightPlanXml = func () {
|
|
||||||
g_wptCount = 0;
|
|
||||||
|
|
||||||
var location = getAirportAndRunway();
|
|
||||||
if (location == nil) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_fpFileHandler = io.open(PATH_FLIGHTPLAN, "w");
|
|
||||||
io.write(
|
|
||||||
g_fpFileHandler,
|
|
||||||
"<?xml version=\"1.0\"?>\n\n" ~
|
|
||||||
"<!-- This file is generated automatically by the Aerotow Everywhere add-on -->\n\n" ~
|
|
||||||
"<PropertyList>\n" ~
|
|
||||||
" <flightplan>\n"
|
|
||||||
);
|
|
||||||
|
|
||||||
var perf = getAircraftPerformance();
|
|
||||||
|
|
||||||
initAircraftVariable(location.airport, location.runway, 1);
|
|
||||||
|
|
||||||
# Start at 2 o'clock from the glider...
|
|
||||||
# Inital ktas must be >= 1.0
|
|
||||||
addWptGround({"hdgChange": 60, "dist": 25}, {"altChange": 0, "ktas": 5});
|
|
||||||
|
|
||||||
# Reset coord and heading
|
|
||||||
initAircraftVariable(location.airport, location.runway, 0);
|
|
||||||
|
|
||||||
var gliderOffsetM = getGliderOffsetFromRunwayThreshold(location.runway);
|
|
||||||
|
|
||||||
# ... and line up with the runway
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 30 + gliderOffsetM}, {"altChange": 0, "ktas": 2.5});
|
|
||||||
|
|
||||||
# Rolling
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10}, {"altChange": 0, "ktas": 5});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 20}, {"altChange": 0, "ktas": 5});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 20}, {"altChange": 0, "ktas": perf.speed / 6});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10}, {"altChange": 0, "ktas": perf.speed / 5});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 4});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 3.5});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 3});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 2.5});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 2});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 1.75});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 1.5});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed / 1.25});
|
|
||||||
addWptGround({"hdgChange": 0, "dist": 10 * perf.rolling}, {"altChange": 0, "ktas": perf.speed});
|
|
||||||
|
|
||||||
# Takeof
|
|
||||||
addWptAir({"hdgChange": 0, "dist": 100 * perf.rolling}, {"elevationPlus": 3, "ktas": perf.speed * 1.05});
|
|
||||||
addWptAir({"hdgChange": 0, "dist": 100}, {"altChange": perf.vs / 10, "ktas": perf.speed * 1.025});
|
|
||||||
|
|
||||||
var speedInc = 1.0;
|
|
||||||
foreach (var wptNode; props.globals.getNode(ADDON_NODE_PATH ~ "/addon-devel/route").getChildren("wpt")) {
|
|
||||||
var dist = wptNode.getChild("distance-m").getValue();
|
|
||||||
if (dist <= 0.0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hdgChange = wptNode.getChild("heading-change").getValue();
|
|
||||||
var altChange = getAltChange(perf.vs, dist);
|
|
||||||
|
|
||||||
speedInc = speedInc + ((dist / DISTANCE_DETERMINANT) * 0.025);
|
|
||||||
var ktas = perf.speed * speedInc;
|
|
||||||
if (ktas > perf.speedLimit) {
|
|
||||||
ktas = perf.speedLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
addWptAir({"hdgChange": hdgChange, "dist": dist}, {"altChange": altChange, "ktas": ktas});
|
|
||||||
}
|
|
||||||
|
|
||||||
addWptEnd();
|
|
||||||
|
|
||||||
io.write(
|
|
||||||
g_fpFileHandler,
|
|
||||||
" </flightplan>\n" ~
|
|
||||||
"</PropertyList>\n\n"
|
|
||||||
);
|
|
||||||
io.close(g_fpFileHandler);
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Return hash with "vs", "speed", "speedLimit", "rolling".
|
|
||||||
#
|
|
||||||
var getAircraftPerformance = func (isRouteMode = 0) {
|
|
||||||
# Cub
|
|
||||||
# Cruise Speed 61 kt
|
|
||||||
# Max Speed 106 kt
|
|
||||||
# Approach speed 44-52 kt
|
|
||||||
# Stall speed 33 kt
|
|
||||||
|
|
||||||
# Robin DR 400
|
|
||||||
# Cruise Speed 134 kt
|
|
||||||
# Max speeed 166 kt
|
|
||||||
# Stall speed 51 kt
|
|
||||||
# Rate of climb: 825 ft/min
|
|
||||||
|
|
||||||
# Cessna 182
|
|
||||||
# Cruise Speed 145 kt
|
|
||||||
# Max speeed 175 kt
|
|
||||||
# Stall speed 50 kt
|
|
||||||
# Best climb: 924 ft/min
|
|
||||||
|
|
||||||
var aiModel = getSelectedAircraft(isRouteMode);
|
|
||||||
if (aiModel == "DR400" or aiModel == "Robin DR400") {
|
|
||||||
return {
|
|
||||||
"vs": 285, # vertical speed in ft per DISTANCE_DETERMINANT m
|
|
||||||
"speed": 70, # take-off speed
|
|
||||||
"speedLimit": 75, # max speed
|
|
||||||
"rolling": 2, # factor for rolling
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aiModel == "c182" or aiModel == "Cessna 182") {
|
|
||||||
return {
|
|
||||||
"vs": 295, # ft per DISTANCE_DETERMINANT m
|
|
||||||
"speed": 75,
|
|
||||||
"speedLimit": 80,
|
|
||||||
"rolling": 2.2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cub
|
|
||||||
return {
|
|
||||||
"vs": 200, # ft per DISTANCE_DETERMINANT m
|
|
||||||
"speed": 55,
|
|
||||||
"speedLimit": 60,
|
|
||||||
"rolling": 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Return how much the altitide increases for a given vertical speed and distance
|
|
||||||
#
|
|
||||||
# vs - vertical speed for DISTANCE_DETERMINANT m
|
|
||||||
# distance - distance in meters
|
|
||||||
#
|
|
||||||
var getAltChange = func (vs, distance) {
|
|
||||||
return vs * (distance / DISTANCE_DETERMINANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Initialize AI aircraft variable
|
|
||||||
#
|
|
||||||
# airport - Object from airportinfo().
|
|
||||||
# runway - Object of runway from which the glider start.
|
|
||||||
# isGliderPos - Pass 1 for set AI aircraft's coordinates as glider position, 0 set coordinates as runway threshold.
|
|
||||||
#
|
|
||||||
var initAircraftVariable = func (airport, runway, isGliderPos = 1) {
|
|
||||||
var gliderCoord = geo.aircraft_position();
|
|
||||||
|
|
||||||
# Set coordinates as glider position or runway threshold
|
|
||||||
g_coord = isGliderPos
|
|
||||||
? gliderCoord
|
|
||||||
: geo.Coord.new().set_latlon(runway.lat, runway.lon);
|
|
||||||
|
|
||||||
# Set airplane heading as runway heading
|
|
||||||
g_heading = runway.heading;
|
|
||||||
|
|
||||||
# Set AI airplane altitude as glider altitude (assumed it's on the ground).
|
|
||||||
# It is more accurate than airport.elevation.
|
|
||||||
g_altitude = gliderCoord.alt() * globals.M2FT;
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Get distance from glider to runway threshold e.g. in case that the user taxi from the runway threshold
|
|
||||||
#
|
|
||||||
# runway - Object of runway from which the glider start
|
|
||||||
# Return the distance in metres, of the glider's displacement from the runway threshold.
|
|
||||||
#
|
|
||||||
var getGliderOffsetFromRunwayThreshold = func (runway) {
|
|
||||||
var gliderCoord = geo.aircraft_position();
|
|
||||||
var rwyThreshold = geo.Coord.new().set_latlon(runway.lat, runway.lon);
|
|
||||||
|
|
||||||
return rwyThreshold.distance_to(gliderCoord);
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Get the minimum runway length required, in meters
|
|
||||||
#
|
|
||||||
var getMinRunwayLength = func () {
|
|
||||||
var aiModel = getSelectedAircraft();
|
|
||||||
if (aiModel == "DR400") {
|
|
||||||
return 470;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aiModel == "c182") {
|
|
||||||
return 508;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 280;
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Add new waypoint on ground
|
|
||||||
#
|
|
||||||
# coordOffset - Hash for calculate next coordinates (lat, lon), with following fields:
|
|
||||||
# {
|
|
||||||
# hdgChange - How the aircraft's heading supposed to change? 0 - keep the same heading.
|
|
||||||
# dis - Distance in meters to calculate next waypoint coordinates.
|
|
||||||
# }
|
|
||||||
# performance - Hash with following fields:
|
|
||||||
# {
|
|
||||||
# altChange - How the aircraft's altitude is supposed to change? 0 - keep the same altitude.
|
|
||||||
# ktas - True air speed of AI plane at the waypoint.
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
var addWptGround = func (coordOffset, performance) {
|
|
||||||
wrireWpt(nil, coordOffset, performance, "ground");
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Add new waypoint in air
|
|
||||||
#
|
|
||||||
var addWptAir = func (coordOffset, performance) {
|
|
||||||
wrireWpt(nil, coordOffset, performance, "air");
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Add "WAIT" waypoint
|
|
||||||
#
|
|
||||||
# sec - Number of seconds for wait
|
|
||||||
#
|
|
||||||
var addWptWait = func (sec) {
|
|
||||||
wrireWpt("WAIT", {}, {}, nil, sec);
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Add "END" waypoint
|
|
||||||
#
|
|
||||||
var addWptEnd = func () {
|
|
||||||
wrireWpt("END", {}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Write waypoint to flight plan file
|
|
||||||
#
|
|
||||||
# name - The name of waypoint
|
|
||||||
# coordOffset.hdgChange - How the aircraft's heading supposed to change?
|
|
||||||
# coordOffset.dist - Distance in meters to calculate next waypoint coordinates
|
|
||||||
# performance.altChange - How the aircraft's altitude is supposed to change?
|
|
||||||
# performance.elevationPlus - Set aircraft altitude as current terrain elevation + given value in feets.
|
|
||||||
# It's best to use for the first point in the air to avoid the plane collapsing into
|
|
||||||
# the ground in a bumpy airport
|
|
||||||
# performance.ktas - True air speed of AI plane at the waypoint
|
|
||||||
# groundAir - Allowed value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
|
|
||||||
# sec - Number of seconds for "WAIT" waypoint
|
|
||||||
#
|
|
||||||
var wrireWpt = func (
|
|
||||||
name,
|
|
||||||
coordOffset,
|
|
||||||
performance,
|
|
||||||
groundAir = nil,
|
|
||||||
sec = nil
|
|
||||||
) {
|
|
||||||
var coord = nil;
|
|
||||||
if (contains(coordOffset, "hdgChange") and contains(coordOffset, "dist")) {
|
|
||||||
g_heading = g_heading + coordOffset.hdgChange;
|
|
||||||
if (g_heading < 0) {
|
|
||||||
g_heading = 360 + g_heading;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (g_heading > 360) {
|
|
||||||
g_heading = g_heading - 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_coord.apply_course_distance(g_heading, coordOffset.dist);
|
|
||||||
coord = g_coord;
|
|
||||||
}
|
|
||||||
|
|
||||||
var alt = nil;
|
|
||||||
if (coord != nil and contains(performance, "elevationPlus")) {
|
|
||||||
var elevation = geo.elevation(coord.lat(), coord.lon());
|
|
||||||
g_altitude = elevation == nil
|
|
||||||
? g_altitude + performance.elevationPlus
|
|
||||||
: elevation * globals.M2FT + performance.elevationPlus;
|
|
||||||
alt = g_altitude;
|
|
||||||
}
|
|
||||||
else if (contains(performance, "altChange")) {
|
|
||||||
g_altitude = g_altitude + performance.altChange;
|
|
||||||
alt = g_altitude;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ktas = contains(performance, "ktas") ? performance.ktas : nil;
|
|
||||||
|
|
||||||
name = name == nil ? g_wptCount : name;
|
|
||||||
var data = getWptString(name, coord, alt, ktas, groundAir, sec);
|
|
||||||
|
|
||||||
io.write(g_fpFileHandler, data);
|
|
||||||
|
|
||||||
g_wptCount = g_wptCount + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Get single waypoint data as a string.
|
|
||||||
#
|
|
||||||
# name - Name of waypoint. Special names are: "WAIT", "END".
|
|
||||||
# coord - The Coord object
|
|
||||||
# alt - Altitude AMSL of AI plane
|
|
||||||
# ktas - True air speed of AI plane
|
|
||||||
# groundAir - Allowe value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
|
|
||||||
# sec - Number of seconds for "WAIT" waypoint
|
|
||||||
#
|
|
||||||
var getWptString = func (name, coord = nil, alt = nil, ktas = nil, groundAir = nil, sec = nil) {
|
|
||||||
var str = " <wpt>\n"
|
|
||||||
~ " <name>" ~ name ~ "</name>\n";
|
|
||||||
|
|
||||||
if (coord != nil) {
|
|
||||||
str = str ~ " <lat>" ~ coord.lat() ~ "</lat>\n";
|
|
||||||
str = str ~ " <lon>" ~ coord.lon() ~ "</lon>\n";
|
|
||||||
str = str ~ " <!-- " ~ coord.lat() ~ "," ~ coord.lon() ~ " -->\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alt != nil) {
|
|
||||||
# str = str ~ " <alt>" ~ alt ~ "</alt>\n";
|
|
||||||
str = str ~ " <crossat>" ~ alt ~ "</crossat>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ktas != nil) {
|
|
||||||
str = str ~ " <ktas>" ~ ktas ~ "</ktas>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groundAir != nil) {
|
|
||||||
var onGround = groundAir == "ground" ? "true" : "false";
|
|
||||||
str = str ~ " <on-ground>" ~ onGround ~ "</on-ground>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sec != nil) {
|
|
||||||
str = str ~ " <time-sec>" ~ sec ~ "</time-sec>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return str ~ " </wpt>\n";
|
|
||||||
}
|
|
||||||
|
@ -189,7 +189,9 @@
|
|||||||
</binding>
|
</binding>
|
||||||
<binding>
|
<binding>
|
||||||
<command>nasal</command>
|
<command>nasal</command>
|
||||||
<script><![CDATA[thermal.add();]]></script>
|
<script><![CDATA[
|
||||||
|
aerotow.g_Aerotow.thermal.add();
|
||||||
|
]]></script>
|
||||||
</binding>
|
</binding>
|
||||||
<binding>
|
<binding>
|
||||||
<command>dialog-close</command>
|
<command>dialog-close</command>
|
||||||
|
@ -516,7 +516,9 @@
|
|||||||
<equal>true</equal>
|
<equal>true</equal>
|
||||||
<binding>
|
<binding>
|
||||||
<command>nasal</command>
|
<command>nasal</command>
|
||||||
<script><![CDATA[aerotow.initialFlightPlan();]]></script>
|
<script><![CDATA[
|
||||||
|
aerotow.g_Aerotow.scenario.flightPlan.initial();
|
||||||
|
]]></script>
|
||||||
</binding>
|
</binding>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
54
messages.nas
54
messages.nas
@ -1,54 +0,0 @@
|
|||||||
#
|
|
||||||
# Aerotow Everywhere - Add-on for FlightGear
|
|
||||||
#
|
|
||||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
|
||||||
#
|
|
||||||
# Copyright (C) 2022 Roman Ludwicki
|
|
||||||
#
|
|
||||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
|
||||||
# under the GNU Public License v3 (GPLv3)
|
|
||||||
#
|
|
||||||
|
|
||||||
#
|
|
||||||
# Display given message as OK.
|
|
||||||
#
|
|
||||||
# message - The text message to display on the screen and read by speech synthesizer.
|
|
||||||
#
|
|
||||||
var displayOk = func (message) {
|
|
||||||
display(message, "ok");
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Display given message as an error.
|
|
||||||
#
|
|
||||||
# message - The text message to display on the screen and read by speech synthesizer.
|
|
||||||
#
|
|
||||||
var displayError = func (message) {
|
|
||||||
display(message, "error");
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Display given message.
|
|
||||||
#
|
|
||||||
# message - The text message to display on the screen and read by speech synthesizer.
|
|
||||||
# type - The type of message. It can take values as "ok" or "error".
|
|
||||||
#
|
|
||||||
var display = func (message, type) {
|
|
||||||
# Print to console
|
|
||||||
print("Aerotow Everywhere add-on: " ~ message);
|
|
||||||
|
|
||||||
# Read the message by speech synthesizer
|
|
||||||
props.globals.getNode("/sim/sound/voices/ai-plane").setValue(message);
|
|
||||||
|
|
||||||
# Display message on the screen
|
|
||||||
var durationInSec = int(size(message) / 12) + 3;
|
|
||||||
var window = screen.window.new(nil, -40, 10, durationInSec);
|
|
||||||
window.bg = [0.0, 0.0, 0.0, 0.40];
|
|
||||||
|
|
||||||
window.fg = type == "error"
|
|
||||||
? [1.0, 0.0, 0.0, 1]
|
|
||||||
: [0.0, 1.0, 0.0, 1];
|
|
||||||
|
|
||||||
window.align = "center";
|
|
||||||
window.write(message);
|
|
||||||
}
|
|
184
nasal/aircraft.nas
Normal file
184
nasal/aircraft.nas
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#
|
||||||
|
# Aerotow Everywhere - Add-on for FlightGear
|
||||||
|
#
|
||||||
|
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 Roman Ludwicki
|
||||||
|
#
|
||||||
|
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||||
|
# under the GNU Public License v3 (GPLv3)
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Parent object of Aircraft
|
||||||
|
#
|
||||||
|
var Aircraft = {
|
||||||
|
#
|
||||||
|
# Constants
|
||||||
|
#
|
||||||
|
DISTANCE_DETERMINANT: 1000, # meters
|
||||||
|
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
# vs - vertical speed in ft per DISTANCE_DETERMINANT m
|
||||||
|
# seed - take-off speed
|
||||||
|
# speedLimit - max speed
|
||||||
|
# rolling - factor for rolling
|
||||||
|
# minRwyLength - minimum runway length required, in meters
|
||||||
|
# name - full name of aircraft used in route dialog
|
||||||
|
# nameMenuCall - short name of aircraft for call a plane from menu
|
||||||
|
# modelPath - Path to the aircraft model
|
||||||
|
#
|
||||||
|
new: func (vs, speed, speedLimit, rolling, minRwyLength, name, nameMenuCall, modelPath) {
|
||||||
|
var obj = { parents: [Aircraft] };
|
||||||
|
|
||||||
|
obj.vs = vs;
|
||||||
|
obj.speed = speed;
|
||||||
|
obj.speedLimit = speedLimit;
|
||||||
|
obj.rolling = rolling;
|
||||||
|
obj.minRwyLength = minRwyLength;
|
||||||
|
obj.name = name;
|
||||||
|
obj.nameMenuCall = nameMenuCall;
|
||||||
|
obj.modelPath = modelPath;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Check that given name match to aircraft name
|
||||||
|
#
|
||||||
|
# name - Name of aircraft to check.
|
||||||
|
#
|
||||||
|
# Return 1 when match, otherwise 0.
|
||||||
|
#
|
||||||
|
isModelName: func (name) {
|
||||||
|
return name == me.name or name == me.nameMenuCall;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Return how much the altitide increases for a given vertical speed and distance
|
||||||
|
#
|
||||||
|
# distance - distance in meters
|
||||||
|
#
|
||||||
|
getAltChange: func (distance) {
|
||||||
|
return me.vs * (distance / Aircraft.DISTANCE_DETERMINANT);
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Return selected Aircraft object
|
||||||
|
#
|
||||||
|
# addon - Addon object
|
||||||
|
# isRouteMode - Use 1 to get the plane for the "Aerotow Route" dialog,
|
||||||
|
# use 0 (default) for call the airplane for towing.
|
||||||
|
#
|
||||||
|
getSelected: func (addon, isRouteMode = 0) {
|
||||||
|
var name = Aircraft.getSelectedAircraftName(addon, isRouteMode);
|
||||||
|
foreach (var aircraft; g_Aircrafts) {
|
||||||
|
if (aircraft.isModelName(name)) {
|
||||||
|
return aircraft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fist as default
|
||||||
|
return g_Aircrafts[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Return name of selected aircraft. Possible values depend of isRouteMode: "Cub", "DR400", "c182".
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# addon - Addon object
|
||||||
|
# isRouteMode - Use 1 to get the plane for the "Aerotow Route" dialog,
|
||||||
|
# use 0 (default) for call the airplane for towing.
|
||||||
|
#
|
||||||
|
getSelectedAircraftName: func (addon, isRouteMode = 0) {
|
||||||
|
if (isRouteMode) {
|
||||||
|
return getprop(addon.node.getPath() ~ "/addon-devel/route/ai-model") or g_Aircrafts[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getprop(addon.node.getPath() ~ "/addon-devel/ai-model") or g_Aircrafts[0].nameMenuCall;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#
|
||||||
|
# Cub
|
||||||
|
# Cruise Speed 61 kt
|
||||||
|
# Max Speed 106 kt
|
||||||
|
# Approach speed 44-52 kt
|
||||||
|
# Stall speed 33 kt
|
||||||
|
#
|
||||||
|
var AircraftCub = {
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
new: func () {
|
||||||
|
return {
|
||||||
|
parents: [Aircraft],
|
||||||
|
vs: 200,
|
||||||
|
speed: 55,
|
||||||
|
speedLimit: 60,
|
||||||
|
rolling: 1,
|
||||||
|
minRwyLength: 280,
|
||||||
|
name: "Piper J3 Cub",
|
||||||
|
nameMenuCall: "Cub",
|
||||||
|
modelPath: "Aircraft/Aerotow/Cub/Models/Cub-ai.xml",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#
|
||||||
|
# Robin DR 400
|
||||||
|
# Cruise Speed 134 kt
|
||||||
|
# Max speeed 166 kt
|
||||||
|
# Stall speed 51 kt
|
||||||
|
# Rate of climb: 825 ft/min
|
||||||
|
#
|
||||||
|
var AircraftRobin = {
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
new: func () {
|
||||||
|
return {
|
||||||
|
parents: [Aircraft],
|
||||||
|
vs: 285,
|
||||||
|
speed: 70,
|
||||||
|
speedLimit: 75,
|
||||||
|
rolling: 2,
|
||||||
|
minRwyLength: 470,
|
||||||
|
name: "Robin DR400",
|
||||||
|
nameMenuCall: "DR400",
|
||||||
|
modelPath: "Aircraft/Aerotow/DR400/Models/dr400-ai.xml",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#
|
||||||
|
# Cessna 182
|
||||||
|
# Cruise Speed 145 kt
|
||||||
|
# Max speeed 175 kt
|
||||||
|
# Stall speed 50 kt
|
||||||
|
# Best climb: 924 ft/min
|
||||||
|
#
|
||||||
|
var AircraftC182 = {
|
||||||
|
new: func () {
|
||||||
|
return {
|
||||||
|
parents: [Aircraft],
|
||||||
|
vs: 295,
|
||||||
|
speed: 75,
|
||||||
|
speedLimit: 80,
|
||||||
|
rolling: 2.2,
|
||||||
|
minRwyLength: 508,
|
||||||
|
name: "Cessna 182",
|
||||||
|
nameMenuCall: "c182",
|
||||||
|
modelPath: "Aircraft/Aerotow/c182/Models/c182-ai.xml",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create Aircraft objects
|
||||||
|
var g_Aircrafts = [
|
||||||
|
AircraftCub.new(),
|
||||||
|
AircraftRobin.new(),
|
||||||
|
AircraftC182.new(),
|
||||||
|
];
|
88
nasal/dialogs/route.nas
Normal file
88
nasal/dialogs/route.nas
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
#
|
||||||
|
# Aerotow Everywhere - Add-on for FlightGear
|
||||||
|
#
|
||||||
|
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 Roman Ludwicki
|
||||||
|
#
|
||||||
|
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||||
|
# under the GNU Public License v3 (GPLv3)
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Object for hande Route Dialog
|
||||||
|
#
|
||||||
|
var RouteDialog = {
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
# addon - Addon object
|
||||||
|
#
|
||||||
|
new: func (addon) {
|
||||||
|
var obj = { parents: [RouteDialog] };
|
||||||
|
|
||||||
|
obj.addon = addon;
|
||||||
|
obj.addonNodePath = addon.node.getPath();
|
||||||
|
|
||||||
|
obj.maxRouteWaypoints = 10;
|
||||||
|
obj.listeners = [];
|
||||||
|
|
||||||
|
# Set listener for aerotow combo box value in route dialog for recalculate altitude change
|
||||||
|
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/route/ai-model", func () {
|
||||||
|
obj.calculateAltChangeAndTotals();
|
||||||
|
}));
|
||||||
|
|
||||||
|
# Set listeners for distance fields for calculate altitude change
|
||||||
|
for (var i = 0; i < obj.maxRouteWaypoints; i = i + 1) {
|
||||||
|
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/route/wpt[" ~ i ~ "]/distance-m", func () {
|
||||||
|
obj.calculateAltChangeAndTotals();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Destructor
|
||||||
|
#
|
||||||
|
del: func () {
|
||||||
|
foreach (var listener; me.listeners) {
|
||||||
|
removelistener(listener);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Calculate total distance and altitude and put in to property tree
|
||||||
|
#
|
||||||
|
calculateAltChangeAndTotals: func () {
|
||||||
|
var totalDistance = 0.0;
|
||||||
|
var totalAlt = 0.0;
|
||||||
|
var isEnd = 0;
|
||||||
|
|
||||||
|
var isRouteMode = 1;
|
||||||
|
var aircraft = Aircraft.getSelected(me.addon, isRouteMode);
|
||||||
|
|
||||||
|
for (var i = 0; i < me.maxRouteWaypoints; i = i + 1) {
|
||||||
|
var distance = getprop(me.addonNodePath ~ "/addon-devel/route/wpt[" ~ i ~ "]/distance-m");
|
||||||
|
if (distance == nil) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var altChange = aircraft.getAltChange(distance);
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/wpt[" ~ i ~ "]/alt-change-agl-ft", altChange);
|
||||||
|
|
||||||
|
if (!isEnd) {
|
||||||
|
if (distance > 0.0) {
|
||||||
|
totalDistance = totalDistance + distance;
|
||||||
|
totalAlt = totalAlt + altChange;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isEnd = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/total/distance", totalDistance);
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/total/alt", totalAlt);
|
||||||
|
},
|
||||||
|
};
|
93
nasal/dialogs/thermal.nas
Normal file
93
nasal/dialogs/thermal.nas
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
#
|
||||||
|
# Aerotow Everywhere - Add-on for FlightGear
|
||||||
|
#
|
||||||
|
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 Roman Ludwicki
|
||||||
|
#
|
||||||
|
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||||
|
# under the GNU Public License v3 (GPLv3)
|
||||||
|
#
|
||||||
|
|
||||||
|
var Thermal = {
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
# addon - Addon object
|
||||||
|
# message - Message object
|
||||||
|
#
|
||||||
|
new: func (addon, message) {
|
||||||
|
var obj = { parents: [Thermal] };
|
||||||
|
|
||||||
|
obj.addon = addon;
|
||||||
|
obj.addonNodePath = addon.node.getPath();
|
||||||
|
obj.message = message;
|
||||||
|
obj.listeners = [];
|
||||||
|
|
||||||
|
# Listener for calculate distance from meters to nautical miles.
|
||||||
|
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/add-thermal/distance-m", func (node) {
|
||||||
|
setprop(obj.addonNodePath ~ "/addon-devel/add-thermal/distance-nm", node.getValue() * globals.M2NM);
|
||||||
|
}));
|
||||||
|
|
||||||
|
# Listener for calculate strength from ft/s to m/s.
|
||||||
|
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/add-thermal/strength-fps", func (node) {
|
||||||
|
setprop(obj.addonNodePath ~ "/addon-devel/add-thermal/strength-mps", node.getValue() * globals.FPS2KT * globals.KT2MPS);
|
||||||
|
}));
|
||||||
|
|
||||||
|
# Listener for calculate diameter from ft to m.
|
||||||
|
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/add-thermal/diameter-ft", func (node) {
|
||||||
|
setprop(obj.addonNodePath ~ "/addon-devel/add-thermal/diameter-m", node.getValue() * globals.FT2M);
|
||||||
|
}));
|
||||||
|
|
||||||
|
# Listener for calculate height from ft to m.
|
||||||
|
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/add-thermal/height-msl", func (node) {
|
||||||
|
setprop(obj.addonNodePath ~ "/addon-devel/add-thermal/height-msl-m", node.getValue() * globals.FT2M);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Destructor
|
||||||
|
#
|
||||||
|
del: func () {
|
||||||
|
foreach (var listener; me.listeners) {
|
||||||
|
removelistener(listener);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add thermal 300 m before glider position.
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
add: func () {
|
||||||
|
var heading = getprop("/orientation/heading-deg") or 0;
|
||||||
|
var distance = getprop(me.addonNodePath ~ "/addon-devel/add-thermal/distance-m") or 300;
|
||||||
|
|
||||||
|
var position = geo.aircraft_position();
|
||||||
|
position.apply_course_distance(heading, distance);
|
||||||
|
|
||||||
|
# Get random layer from 1 to 4
|
||||||
|
var layer = int(rand() * 4) + 1;
|
||||||
|
|
||||||
|
var args = props.Node.new({
|
||||||
|
"type": "thermal",
|
||||||
|
"model": "Models/Weather/altocumulus_layer" ~ layer ~ ".xml",
|
||||||
|
"latitude": position.lat(),
|
||||||
|
"longitude": position.lon(),
|
||||||
|
"strength-fps": getprop(me.addonNodePath ~ "/addon-devel/add-thermal/strength-fps") or 16.0,
|
||||||
|
"diameter-ft": getprop(me.addonNodePath ~ "/addon-devel/add-thermal/diameter-ft") or 4000,
|
||||||
|
"height-msl": getprop(me.addonNodePath ~ "/addon-devel/add-thermal/height-msl") or 9000,
|
||||||
|
"search-order": "DATA_ONLY"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fgcommand("add-aiobject", args)) {
|
||||||
|
me.message.success("The thermal has been added");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
me.message.error("Adding thermal failed");
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
};
|
423
nasal/flight-plan.nas
Normal file
423
nasal/flight-plan.nas
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
#
|
||||||
|
# Aerotow Everywhere - Add-on for FlightGear
|
||||||
|
#
|
||||||
|
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 Roman Ludwicki
|
||||||
|
#
|
||||||
|
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||||
|
# under the GNU Public License v3 (GPLv3)
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Flight Plan object
|
||||||
|
#
|
||||||
|
var FlightPlan = {
|
||||||
|
#
|
||||||
|
# Constants
|
||||||
|
#
|
||||||
|
FILENAME_FLIGHTPLAN: "aerotown-addon-flightplan.xml",
|
||||||
|
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
# addon - Addon object
|
||||||
|
# message - Message object
|
||||||
|
# routeDialog - RouteDialog object
|
||||||
|
#
|
||||||
|
new: func (addon, message, routeDialog) {
|
||||||
|
var obj = { parents: [FlightPlan] };
|
||||||
|
|
||||||
|
obj.addon = addon;
|
||||||
|
obj.message = message;
|
||||||
|
obj.routeDialog = routeDialog;
|
||||||
|
|
||||||
|
obj.addonNodePath = addon.node.getPath();
|
||||||
|
|
||||||
|
obj.wptCount = 0;
|
||||||
|
obj.fpFileHandler = nil; # Handler for wrire flight plan to file
|
||||||
|
obj.coord = nil; # Coordinates for flight plan
|
||||||
|
obj.heading = nil; # AI plane heading
|
||||||
|
obj.altitude = nil; # AI plane altitude
|
||||||
|
|
||||||
|
obj.flightPlanPath = addon.storagePath ~ "/AI/FlightPlans/" ~ FlightPlan.FILENAME_FLIGHTPLAN;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Get airport an runway hash where the glider is located.
|
||||||
|
#
|
||||||
|
# Return hash with "airport" and "runway", otherwise nil.
|
||||||
|
#
|
||||||
|
getAirportAndRunway: func () {
|
||||||
|
var icao = getprop("/sim/airport/closest-airport-id");
|
||||||
|
if (icao == nil) {
|
||||||
|
me.message.error("Airport code cannot be obtained.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
var runwayName = getprop("/sim/atc/runway");
|
||||||
|
if (runwayName == nil) {
|
||||||
|
me.message.error("Runway name cannot be obtained.");
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
var airport = airportinfo(icao);
|
||||||
|
|
||||||
|
if (!contains(airport.runways, runwayName)) {
|
||||||
|
me.message.error("The " ~ icao ~" airport does not have runway " ~ runwayName);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
var runway = airport.runways[runwayName];
|
||||||
|
|
||||||
|
var minRwyLength = Aircraft.getSelected(me.addon).minRwyLength;
|
||||||
|
if (runway.length < minRwyLength) {
|
||||||
|
me.message.error(
|
||||||
|
"This runway is too short. Please choose a longer one than " ~ minRwyLength ~ " m "
|
||||||
|
~ "(" ~ math.round(minRwyLength * globals.M2FT) ~ " ft)."
|
||||||
|
);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"airport": airport,
|
||||||
|
"runway": runway,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Initialize flight plan and set it to property tree
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
initial: func () {
|
||||||
|
var location = me.getAirportAndRunway();
|
||||||
|
if (location == nil) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aircraft = Aircraft.getSelected(me.addon);
|
||||||
|
|
||||||
|
me.initAircraftVariable(location.airport, location.runway, 0);
|
||||||
|
|
||||||
|
# inittial readonly waypoint
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/init-wpt/heading-change", me.heading);
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/init-wpt/distance-m", 100);
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/init-wpt/alt-change-agl-ft", aircraft.vs / 10);
|
||||||
|
|
||||||
|
# in air
|
||||||
|
var wptData = [
|
||||||
|
{"hdgChange": 0, "dist": 5000, "altChange": aircraft.vs * 5},
|
||||||
|
{"hdgChange": -90, "dist": 1000, "altChange": aircraft.vs},
|
||||||
|
{"hdgChange": -90, "dist": 1000, "altChange": aircraft.vs},
|
||||||
|
{"hdgChange": 0, "dist": 5000, "altChange": aircraft.vs * 5},
|
||||||
|
{"hdgChange": -90, "dist": 1500, "altChange": aircraft.vs * 1.5},
|
||||||
|
{"hdgChange": -90, "dist": 1000, "altChange": aircraft.vs},
|
||||||
|
{"hdgChange": 0, "dist": 5000, "altChange": aircraft.vs * 5},
|
||||||
|
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||||
|
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||||
|
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||||
|
];
|
||||||
|
|
||||||
|
# Default route
|
||||||
|
# ^ - airport with heading direction to north
|
||||||
|
# 1 - 1st waypoint
|
||||||
|
# 2 - 2nd waypoint, etc.
|
||||||
|
#
|
||||||
|
# 2 . . 1 7
|
||||||
|
# . . .
|
||||||
|
# . . .
|
||||||
|
# 3 . .
|
||||||
|
# . . .
|
||||||
|
# . . .
|
||||||
|
# . . .
|
||||||
|
# . . .
|
||||||
|
# . . .
|
||||||
|
# . . .
|
||||||
|
# . ^ 6
|
||||||
|
# . .
|
||||||
|
# . .
|
||||||
|
# 4 . . . . 5
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
foreach (var wpt; wptData) {
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/wpt[" ~ index ~ "]/heading-change", wpt.hdgChange);
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/wpt[" ~ index ~ "]/distance-m", wpt.dist);
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/route/wpt[" ~ index ~ "]/alt-change-agl-ft", wpt.altChange);
|
||||||
|
|
||||||
|
index = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
me.routeDialog.calculateAltChangeAndTotals();
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate the XML file with the flight plane for our plane for AI scenario.
|
||||||
|
# The file will be stored to $Fobj.HOME/Export/aerotown-addon-flightplan.xml.
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
generateXml: func () {
|
||||||
|
me.wptCount = 0;
|
||||||
|
|
||||||
|
var location = me.getAirportAndRunway();
|
||||||
|
if (location == nil) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
me.fpFileHandler = io.open(me.flightPlanPath, "w");
|
||||||
|
io.write(
|
||||||
|
me.fpFileHandler,
|
||||||
|
"<?xml version=\"1.0\"?>\n\n" ~
|
||||||
|
"<!-- This file is generated automatically by the Aerotow Everywhere add-on -->\n\n" ~
|
||||||
|
"<PropertyList>\n" ~
|
||||||
|
" <flightplan>\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
var aircraft = Aircraft.getSelected(me.addon);
|
||||||
|
|
||||||
|
me.initAircraftVariable(location.airport, location.runway, 1);
|
||||||
|
|
||||||
|
# Start at 2 o'clock from the glider...
|
||||||
|
# Inital ktas must be >= 1.0
|
||||||
|
me.addWptGround({"hdgChange": 60, "dist": 25}, {"altChange": 0, "ktas": 5});
|
||||||
|
|
||||||
|
# Reset coord and heading
|
||||||
|
me.initAircraftVariable(location.airport, location.runway, 0);
|
||||||
|
|
||||||
|
var gliderOffsetM = me.getGliderOffsetFromRunwayThreshold(location.runway);
|
||||||
|
|
||||||
|
# ... and line up with the runway
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 30 + gliderOffsetM}, {"altChange": 0, "ktas": 2.5});
|
||||||
|
|
||||||
|
# Rolling
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10}, {"altChange": 0, "ktas": 5});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 20}, {"altChange": 0, "ktas": 5});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 20}, {"altChange": 0, "ktas": aircraft.speed / 6});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10}, {"altChange": 0, "ktas": aircraft.speed / 5});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 4});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 3.5});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 3});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 2.5});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 2});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 1.75});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 1.5});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed / 1.25});
|
||||||
|
me.addWptGround({"hdgChange": 0, "dist": 10 * aircraft.rolling}, {"altChange": 0, "ktas": aircraft.speed});
|
||||||
|
|
||||||
|
# Takeof
|
||||||
|
me.addWptAir({"hdgChange": 0, "dist": 100 * aircraft.rolling}, {"elevationPlus": 3, "ktas": aircraft.speed * 1.05});
|
||||||
|
me.addWptAir({"hdgChange": 0, "dist": 100}, {"altChange": aircraft.vs / 10, "ktas": aircraft.speed * 1.025});
|
||||||
|
|
||||||
|
var speedInc = 1.0;
|
||||||
|
foreach (var wptNode; props.globals.getNode(me.addonNodePath ~ "/addon-devel/route").getChildren("wpt")) {
|
||||||
|
var dist = wptNode.getChild("distance-m").getValue();
|
||||||
|
if (dist <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hdgChange = wptNode.getChild("heading-change").getValue();
|
||||||
|
var altChange = aircraft.getAltChange(dist);
|
||||||
|
|
||||||
|
speedInc = speedInc + ((dist / Aircraft.DISTANCE_DETERMINANT) * 0.025);
|
||||||
|
var ktas = aircraft.speed * speedInc;
|
||||||
|
if (ktas > aircraft.speedLimit) {
|
||||||
|
ktas = aircraft.speedLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
me.addWptAir({"hdgChange": hdgChange, "dist": dist}, {"altChange": altChange, "ktas": ktas});
|
||||||
|
}
|
||||||
|
|
||||||
|
me.addWptEnd();
|
||||||
|
|
||||||
|
io.write(
|
||||||
|
me.fpFileHandler,
|
||||||
|
" </flightplan>\n" ~
|
||||||
|
"</PropertyList>\n\n"
|
||||||
|
);
|
||||||
|
io.close(me.fpFileHandler);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Initialize AI aircraft variable
|
||||||
|
#
|
||||||
|
# airport - Object from airportinfo().
|
||||||
|
# runway - Object of runway from which the glider start.
|
||||||
|
# isGliderPos - Pass 1 for set AI aircraft's coordinates as glider position, 0 set coordinates as runway threshold.
|
||||||
|
#
|
||||||
|
initAircraftVariable: func (airport, runway, isGliderPos = 1) {
|
||||||
|
var gliderCoord = geo.aircraft_position();
|
||||||
|
|
||||||
|
# Set coordinates as glider position or runway threshold
|
||||||
|
me.coord = isGliderPos
|
||||||
|
? gliderCoord
|
||||||
|
: geo.Coord.new().set_latlon(runway.lat, runway.lon);
|
||||||
|
|
||||||
|
# Set airplane heading as runway heading
|
||||||
|
me.heading = runway.heading;
|
||||||
|
|
||||||
|
# Set AI airplane altitude as glider altitude (assumed it's on the ground).
|
||||||
|
# It is more accurate than airport.elevation.
|
||||||
|
me.altitude = gliderCoord.alt() * globals.M2FT;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Get distance from glider to runway threshold e.g. in case that the user taxi from the runway threshold
|
||||||
|
#
|
||||||
|
# runway - Object of runway from which the glider start
|
||||||
|
# Return the distance in metres, of the glider's displacement from the runway threshold.
|
||||||
|
#
|
||||||
|
getGliderOffsetFromRunwayThreshold: func (runway) {
|
||||||
|
var gliderCoord = geo.aircraft_position();
|
||||||
|
var rwyThreshold = geo.Coord.new().set_latlon(runway.lat, runway.lon);
|
||||||
|
|
||||||
|
return rwyThreshold.distance_to(gliderCoord);
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add new waypoint on ground
|
||||||
|
#
|
||||||
|
# coordOffset - Hash for calculate next coordinates (lat, lon), with following fields:
|
||||||
|
# {
|
||||||
|
# hdgChange - How the aircraft's heading supposed to change? 0 - keep the same heading.
|
||||||
|
# dis - Distance in meters to calculate next waypoint coordinates.
|
||||||
|
# }
|
||||||
|
# performance - Hash with following fields:
|
||||||
|
# {
|
||||||
|
# altChange - How the aircraft's altitude is supposed to change? 0 - keep the same altitude.
|
||||||
|
# ktas - True air speed of AI plane at the waypoint.
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
addWptGround: func (coordOffset, performance) {
|
||||||
|
me.wrireWpt(nil, coordOffset, performance, "ground");
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add new waypoint in air
|
||||||
|
#
|
||||||
|
addWptAir: func (coordOffset, performance) {
|
||||||
|
me.wrireWpt(nil, coordOffset, performance, "air");
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add "WAIT" waypoint
|
||||||
|
#
|
||||||
|
# sec - Number of seconds for wait
|
||||||
|
#
|
||||||
|
addWptWait: func (sec) {
|
||||||
|
me.wrireWpt("WAIT", {}, {}, nil, sec);
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add "END" waypoint
|
||||||
|
#
|
||||||
|
addWptEnd: func () {
|
||||||
|
me.wrireWpt("END", {}, {});
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Write waypoint to flight plan file
|
||||||
|
#
|
||||||
|
# name - The name of waypoint
|
||||||
|
# coordOffset.hdgChange - How the aircraft's heading supposed to change?
|
||||||
|
# coordOffset.dist - Distance in meters to calculate next waypoint coordinates
|
||||||
|
# performance.altChange - How the aircraft's altitude is supposed to change?
|
||||||
|
# performance.elevationPlus - Set aircraft altitude as current terrain elevation + given value in feets.
|
||||||
|
# It's best to use for the first point in the air to avoid the plane collapsing into
|
||||||
|
# the ground in a bumpy airport
|
||||||
|
# performance.ktas - True air speed of AI plane at the waypoint
|
||||||
|
# groundAir - Allowed value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
|
||||||
|
# sec - Number of seconds for "WAIT" waypoint
|
||||||
|
#
|
||||||
|
wrireWpt: func (
|
||||||
|
name,
|
||||||
|
coordOffset,
|
||||||
|
performance,
|
||||||
|
groundAir = nil,
|
||||||
|
sec = nil
|
||||||
|
) {
|
||||||
|
var coord = nil;
|
||||||
|
if (contains(coordOffset, "hdgChange") and contains(coordOffset, "dist")) {
|
||||||
|
me.heading = me.heading + coordOffset.hdgChange;
|
||||||
|
if (me.heading < 0) {
|
||||||
|
me.heading = 360 + me.heading;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (me.heading > 360) {
|
||||||
|
me.heading = me.heading - 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
me.coord.apply_course_distance(me.heading, coordOffset.dist);
|
||||||
|
coord = me.coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alt = nil;
|
||||||
|
if (coord != nil and contains(performance, "elevationPlus")) {
|
||||||
|
var elevation = geo.elevation(coord.lat(), coord.lon());
|
||||||
|
me.altitude = elevation == nil
|
||||||
|
? me.altitude + performance.elevationPlus
|
||||||
|
: elevation * globals.M2FT + performance.elevationPlus;
|
||||||
|
alt = me.altitude;
|
||||||
|
}
|
||||||
|
else if (contains(performance, "altChange")) {
|
||||||
|
me.altitude = me.altitude + performance.altChange;
|
||||||
|
alt = me.altitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ktas = contains(performance, "ktas") ? performance.ktas : nil;
|
||||||
|
|
||||||
|
name = name == nil ? me.wptCount : name;
|
||||||
|
var data = me.getWptString(name, coord, alt, ktas, groundAir, sec);
|
||||||
|
|
||||||
|
io.write(me.fpFileHandler, data);
|
||||||
|
|
||||||
|
me.wptCount = me.wptCount + 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Get single waypoint data as a string.
|
||||||
|
#
|
||||||
|
# name - Name of waypoint. Special names are: "WAIT", "END".
|
||||||
|
# coord - The Coord object
|
||||||
|
# alt - Altitude AMSL of AI plane
|
||||||
|
# ktas - True air speed of AI plane
|
||||||
|
# groundAir - Allowe value: "ground or "air". The "ground" means that AI plane is on the ground, "air" - in air
|
||||||
|
# sec - Number of seconds for "WAIT" waypoint
|
||||||
|
#
|
||||||
|
getWptString: func (name, coord = nil, alt = nil, ktas = nil, groundAir = nil, sec = nil) {
|
||||||
|
var str = " <wpt>\n"
|
||||||
|
~ " <name>" ~ name ~ "</name>\n";
|
||||||
|
|
||||||
|
if (coord != nil) {
|
||||||
|
str = str ~ " <lat>" ~ coord.lat() ~ "</lat>\n";
|
||||||
|
str = str ~ " <lon>" ~ coord.lon() ~ "</lon>\n";
|
||||||
|
str = str ~ " <!-- " ~ coord.lat() ~ "," ~ coord.lon() ~ " -->\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alt != nil) {
|
||||||
|
# str = str ~ " <alt>" ~ alt ~ "</alt>\n";
|
||||||
|
str = str ~ " <crossat>" ~ alt ~ "</crossat>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktas != nil) {
|
||||||
|
str = str ~ " <ktas>" ~ ktas ~ "</ktas>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groundAir != nil) {
|
||||||
|
var onGround = groundAir == "ground" ? "true" : "false";
|
||||||
|
str = str ~ " <on-ground>" ~ onGround ~ "</on-ground>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sec != nil) {
|
||||||
|
str = str ~ " <time-sec>" ~ sec ~ "</time-sec>\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return str ~ " </wpt>\n";
|
||||||
|
},
|
||||||
|
};
|
66
nasal/message.nas
Normal file
66
nasal/message.nas
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#
|
||||||
|
# Aerotow Everywhere - Add-on for FlightGear
|
||||||
|
#
|
||||||
|
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 Roman Ludwicki
|
||||||
|
#
|
||||||
|
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||||
|
# under the GNU Public License v3 (GPLv3)
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Object for display messages
|
||||||
|
#
|
||||||
|
var Message = {
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
new: func () {
|
||||||
|
return { parents: [Message] };
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Display given message as OK.
|
||||||
|
#
|
||||||
|
# message - The text message to display on the screen and read by speech synthesizer.
|
||||||
|
#
|
||||||
|
success: func (message) {
|
||||||
|
me.display(message, "ok");
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Display given message as an error.
|
||||||
|
#
|
||||||
|
# message - The text message to display on the screen and read by speech synthesizer.
|
||||||
|
#
|
||||||
|
error: func (message) {
|
||||||
|
me.display(message, "error");
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Display given message.
|
||||||
|
#
|
||||||
|
# message - The text message to display on the screen and read by speech synthesizer.
|
||||||
|
# type - The type of message. It can take values as "ok" or "error".
|
||||||
|
#
|
||||||
|
display: func (message, type) {
|
||||||
|
# Print to console
|
||||||
|
print("Aerotow Everywhere add-on: " ~ message);
|
||||||
|
|
||||||
|
# Read the message by speech synthesizer
|
||||||
|
props.globals.getNode("/sim/sound/voices/ai-plane").setValue(message);
|
||||||
|
|
||||||
|
# Display message on the screen
|
||||||
|
var durationInSec = int(size(message) / 12) + 3;
|
||||||
|
var window = screen.window.new(nil, -40, 10, durationInSec);
|
||||||
|
window.bg = [0.0, 0.0, 0.0, 0.40];
|
||||||
|
|
||||||
|
window.fg = type == "error"
|
||||||
|
? [1.0, 0.0, 0.0, 1]
|
||||||
|
: [0.0, 1.0, 0.0, 1];
|
||||||
|
|
||||||
|
window.align = "center";
|
||||||
|
window.write(message);
|
||||||
|
},
|
||||||
|
};
|
180
nasal/scenario.nas
Normal file
180
nasal/scenario.nas
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
#
|
||||||
|
# Aerotow Everywhere - Add-on for FlightGear
|
||||||
|
#
|
||||||
|
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 Roman Ludwicki
|
||||||
|
#
|
||||||
|
# Aerotow Everywhere is an Open Source project and it is licensed
|
||||||
|
# under the GNU Public License v3 (GPLv3)
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Scenario object
|
||||||
|
#
|
||||||
|
var Scenario = {
|
||||||
|
#
|
||||||
|
# Constants
|
||||||
|
#
|
||||||
|
SCENARIO_ID: "aerotow_addon",
|
||||||
|
SCENARIO_NAME: "Aerotow Add-on",
|
||||||
|
SCENARIO_DESC: "This scenario starts the towing plane at the airport where the pilot with the glider is located. Use Ctrl-o to hook the plane.",
|
||||||
|
FILENAME_SCENARIO: "aerotown-addon.xml",
|
||||||
|
|
||||||
|
#
|
||||||
|
# Constructor
|
||||||
|
#
|
||||||
|
# addon - Addon object
|
||||||
|
# message - Message object
|
||||||
|
#
|
||||||
|
new: func (addon, message) {
|
||||||
|
var obj = { parents: [Scenario] };
|
||||||
|
|
||||||
|
obj.addon = addon;
|
||||||
|
obj.message = message;
|
||||||
|
|
||||||
|
obj.addonNodePath = addon.node.getPath();
|
||||||
|
|
||||||
|
obj.listeners = [];
|
||||||
|
obj.routeDialog = RouteDialog.new(addon);
|
||||||
|
obj.flightPlan = FlightPlan.new(addon, message, obj.routeDialog);
|
||||||
|
obj.isScenarioLoaded = 0;
|
||||||
|
obj.scenarioPath = addon.storagePath ~ "/" ~ Scenario.FILENAME_SCENARIO;
|
||||||
|
|
||||||
|
obj.flightPlan.initial();
|
||||||
|
|
||||||
|
append(obj.listeners, setlistener("/sim/presets/longitude-deg", func () {
|
||||||
|
# User change airport/runway
|
||||||
|
obj.flightPlan.initial();
|
||||||
|
}));
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Destructor
|
||||||
|
#
|
||||||
|
del: func () {
|
||||||
|
me.routeDialog.del();
|
||||||
|
|
||||||
|
foreach (var listener; me.listeners) {
|
||||||
|
removelistener(listener);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate the XML file with the AI scenario.
|
||||||
|
# The file will be stored to $FG_HOME/Export/aerotown-addon.xml.
|
||||||
|
#
|
||||||
|
generateXml: func () {
|
||||||
|
if (!me.flightPlan.generateXml()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scenarioXml = {
|
||||||
|
"PropertyList": {
|
||||||
|
"scenario": {
|
||||||
|
"name": Scenario.SCENARIO_NAME,
|
||||||
|
"description": Scenario.SCENARIO_DESC,
|
||||||
|
"entry": {
|
||||||
|
"callsign": "FG-TOW",
|
||||||
|
"type": "aircraft",
|
||||||
|
"class": "aerotow-dragger",
|
||||||
|
"model": Aircraft.getSelected(me.addon).modelPath,
|
||||||
|
"flightplan": FlightPlan.FILENAME_FLIGHTPLAN,
|
||||||
|
"repeat": 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var node = props.Node.new(scenarioXml);
|
||||||
|
io.writexml(me.scenarioPath, node);
|
||||||
|
|
||||||
|
me.addScenarioToPropertyList();
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Add our new scenario to the "/sim/ai/scenarios" property list
|
||||||
|
# so that FlightGear will be able to load it by "load-scenario" command.
|
||||||
|
#
|
||||||
|
addScenarioToPropertyList: func () {
|
||||||
|
if (!me.isAlreadyAdded()) {
|
||||||
|
var scenarioData = {
|
||||||
|
"name": Scenario.SCENARIO_NAME,
|
||||||
|
"id": Scenario.SCENARIO_ID,
|
||||||
|
"description": Scenario.SCENARIO_DESC,
|
||||||
|
"path": me.scenarioPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
props.globals.getNode("/sim/ai/scenarios").addChild("scenario").setValues(scenarioData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Return 1 if scenario is already added to "/sim/ai/scenarios" property list, otherwise return 0.
|
||||||
|
#
|
||||||
|
isAlreadyAdded: func () {
|
||||||
|
foreach (var scenario; props.globals.getNode("/sim/ai/scenarios").getChildren("scenario")) {
|
||||||
|
var id = scenario.getChild("id");
|
||||||
|
if (id != nil and id.getValue() == Scenario.SCENARIO_ID) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Load scenario
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
load: func () {
|
||||||
|
var args = props.Node.new({ "name": Scenario.SCENARIO_ID });
|
||||||
|
if (fgcommand("load-scenario", args)) {
|
||||||
|
me.isScenarioLoaded = 1;
|
||||||
|
me.message.success("Let's fly!");
|
||||||
|
|
||||||
|
# Enable engine sound
|
||||||
|
setprop(me.addonNodePath ~ "/addon-devel/sound/enable", 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
me.message.error("Tow failed!");
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unload scenario
|
||||||
|
#
|
||||||
|
# withMessages - Set 1 to display messages.
|
||||||
|
#
|
||||||
|
# Return 1 on successful, otherwise 0.
|
||||||
|
#
|
||||||
|
unload: func (withMessages = 0) {
|
||||||
|
if (me.isScenarioLoaded) {
|
||||||
|
var args = props.Node.new({ "name": Scenario.SCENARIO_ID });
|
||||||
|
if (fgcommand("unload-scenario", args)) {
|
||||||
|
me.isScenarioLoaded = 0;
|
||||||
|
|
||||||
|
if (withMessages) {
|
||||||
|
me.message.success("Aerotown disabled");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withMessages) {
|
||||||
|
me.message.error("Aerotown disable failed");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withMessages) {
|
||||||
|
me.message.success("Aerotown already disabled");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
};
|
90
thermal.nas
90
thermal.nas
@ -1,90 +0,0 @@
|
|||||||
#
|
|
||||||
# Aerotow Everywhere - Add-on for FlightGear
|
|
||||||
#
|
|
||||||
# Written and developer by Roman Ludwicki (PlayeRom, SP-ROM)
|
|
||||||
#
|
|
||||||
# Copyright (C) 2022 Roman Ludwicki
|
|
||||||
#
|
|
||||||
# Aerotow Everywhere is an Open Source project and it is licensed
|
|
||||||
# under the GNU Public License v3 (GPLv3)
|
|
||||||
#
|
|
||||||
|
|
||||||
#
|
|
||||||
# Constants
|
|
||||||
#
|
|
||||||
var ADDON = addons.getAddon("org.flightgear.addons.Aerotow");
|
|
||||||
var ADDON_NODE_PATH = ADDON.node.getPath();
|
|
||||||
|
|
||||||
#
|
|
||||||
# Variables
|
|
||||||
#
|
|
||||||
var g_thermalListeners = [];
|
|
||||||
|
|
||||||
#
|
|
||||||
# Initialize thermal module
|
|
||||||
#
|
|
||||||
var init = func () {
|
|
||||||
# Listener for calculate distance from meters to nautical miles.
|
|
||||||
append(g_thermalListeners, setlistener(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/distance-m", func (node) {
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/distance-nm", node.getValue() * globals.M2NM);
|
|
||||||
}));
|
|
||||||
|
|
||||||
# Listener for calculate strength from ft/s to m/s.
|
|
||||||
append(g_thermalListeners, setlistener(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/strength-fps", func (node) {
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/strength-mps", node.getValue() * globals.FPS2KT * globals.KT2MPS);
|
|
||||||
}));
|
|
||||||
|
|
||||||
# Listener for calculate diameter from ft to m.
|
|
||||||
append(g_thermalListeners, setlistener(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/diameter-ft", func (node) {
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/diameter-m", node.getValue() * globals.FT2M);
|
|
||||||
}));
|
|
||||||
|
|
||||||
# Listener for calculate height from ft to m.
|
|
||||||
append(g_thermalListeners, setlistener(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/height-msl", func (node) {
|
|
||||||
setprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/height-msl-m", node.getValue() * globals.FT2M);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Uninitialize thermal module
|
|
||||||
#
|
|
||||||
var uninit = func () {
|
|
||||||
foreach (var listener; g_thermalListeners) {
|
|
||||||
removelistener(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#
|
|
||||||
# Add thermal 300 m before glider position.
|
|
||||||
#
|
|
||||||
# Return 1 on successful, otherwise 0.
|
|
||||||
#
|
|
||||||
var add = func () {
|
|
||||||
var heading = getprop("/orientation/heading-deg") or 0;
|
|
||||||
var distance = getprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/distance-m") or 300;
|
|
||||||
|
|
||||||
var position = geo.aircraft_position();
|
|
||||||
position.apply_course_distance(heading, distance);
|
|
||||||
|
|
||||||
# Get random layer from 1 to 4
|
|
||||||
var layer = int(rand() * 4) + 1;
|
|
||||||
|
|
||||||
var args = props.Node.new({
|
|
||||||
"type": "thermal",
|
|
||||||
"model": "Models/Weather/altocumulus_layer" ~ layer ~ ".xml",
|
|
||||||
"latitude": position.lat(),
|
|
||||||
"longitude": position.lon(),
|
|
||||||
"strength-fps": getprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/strength-fps") or 16.0,
|
|
||||||
"diameter-ft": getprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/diameter-ft") or 4000,
|
|
||||||
"height-msl": getprop(ADDON_NODE_PATH ~ "/addon-devel/add-thermal/height-msl") or 9000,
|
|
||||||
"search-order": "DATA_ONLY"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fgcommand("add-aiobject", args)) {
|
|
||||||
messages.displayOk("The thermal has been added");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.displayError("Adding thermal failed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user