Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d5bd7c7f5 | ||
|
|
c60097c8cd | ||
|
|
f3d50cc815 | ||
|
|
cbc1888425 | ||
|
|
ea72151d48 |
@@ -1,5 +1,13 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v.1.8.0
|
||||
|
||||
- The tow plane, after completing its route, returns to the runway from which it took off. For a bush take-off it does not land.
|
||||
|
||||
## v.1.7.0
|
||||
|
||||
- Add possibility to set max altitude in route dialog
|
||||
|
||||
## v.1.6.0
|
||||
|
||||
- Add sound support from internal and external view for C-74 aircraft (thanks to wlbragg)
|
||||
|
||||
36
README.md
36
README.md
@@ -28,7 +28,7 @@ You can also take off in the bush. Then the tow plane will position itself in fr
|
||||
|
||||
## How does the AI tow aircraft fly by default?
|
||||
|
||||
The tow plane always takes off in front of your runway and flies along the runway for 5 km, then turns back and flies downwind for 6 km, then turns back again and flies another 6 km. During this flight it is constantly gaining altitude. Then when it has completed the entire set route it turns to unknown direction and flies at a constant altitude.
|
||||
The tow plane always takes off in front of your runway and flies along the runway for 5 km, then turns back and flies downwind for 6 km, then turns back again and flies another 6 km. During this flight it is constantly gaining altitude. Then, after having flown the entire given route, it lands at the airport from which it took off or when start in the bush turns in an unknown direction and flies at a constant altitude. If the aircraft lands at the airport, the scenario starts again, i.e. the aircraft respawn to take-off again.
|
||||
|
||||
You can disconnect from the aircraft at any time, most often by pressing the `o` key (check help of your glider).
|
||||
|
||||
@@ -38,20 +38,20 @@ Default route
|
||||
1 - 1st waypoint
|
||||
2 - 2nd waypoint, etc.
|
||||
|
||||
2 . . 1 7
|
||||
. . .
|
||||
. . .
|
||||
3 . .
|
||||
2 . . 1 5
|
||||
. . .
|
||||
. . .
|
||||
. . .
|
||||
. . .
|
||||
. . .
|
||||
. . .
|
||||
. ^ 6
|
||||
. . .
|
||||
. . .
|
||||
. . .
|
||||
. ^ .
|
||||
. .
|
||||
. .
|
||||
4 . . . . 5
|
||||
3 . . . . 4
|
||||
```
|
||||
|
||||
## Menu of add-on
|
||||
@@ -59,13 +59,15 @@ Default route
|
||||
This add-on add a new item to main menu named `Aerotow Everywhere` with following items:
|
||||
|
||||
1. `Aerotow Route` - display the dialog for change aerotow route.
|
||||
2. `Call for Piper J3 Cub aircraft` - load AI tow sceneraio with Piper J3 Cub.
|
||||
3. `Call for Robin DR400 aircraft` - load AI tow sceneraio with Robin DR400. This aircraft has better performance and can take you higher then Piper Cub.
|
||||
4. `Call for Cessna 182 aircraft` - load AI tow sceneraio with Cessna 182. This aircraft has little bit better performance than Robin.
|
||||
5. `Disable tow aircraft` - unload AI tow sceneraio.
|
||||
6. `Add thermal` - display the dialog for configuring and adding thermal.
|
||||
7. `Help` - display help dialog.
|
||||
8. `About` - display about dialog with add-on information.
|
||||
2. `Towrope Configuration` - display the dialog for change towrope parameters.
|
||||
3. `Call for Piper J3 Cub aircraft` - load AI tow sceneraio with Piper J3 Cub.
|
||||
4. `Call for Robin DR400 aircraft` - load AI tow sceneraio with Robin DR400. This aircraft has better performance and can take you higher then Piper Cub.
|
||||
5. `Call for Cessna 182 aircraft` - load AI tow sceneraio with Cessna 182. This aircraft has little bit better performance than Robin.
|
||||
6. `Call for Douglas C-47 aircraft` - load AI tow sceneraio with Douglas C-47.
|
||||
7. `Disable tow aircraft` - unload AI tow sceneraio.
|
||||
8. `Add thermal` - display the dialog for configuring and adding thermal.
|
||||
9. `Help` - display help dialog.
|
||||
10. `About` - display about dialog with add-on information.
|
||||
|
||||
## Aerotow Route
|
||||
|
||||
@@ -75,6 +77,10 @@ You can change the AI aircraft's default route, for this go to menu `Aerotow Eve
|
||||
|
||||
On the top of the "Aerotow Route" dialog you have selector to change aircraft type. It's only for calculate performance and display how the altitude will change.
|
||||
|
||||
### Max alt (AGL)
|
||||
|
||||
Here you can set limit of AGL altitude. 0 means without limits - the plane will always increase its altitude during its flight. AGL is always in terms of the place you are starting from.
|
||||
|
||||
### Route
|
||||
|
||||
Next you have initial heading, distance and altitude change. "Initial heading" depend of the runway where you are located. The aircraft will always take-off along the runway, a distance of 100 m, where it will gain 20 ft in altitude above the terrain. These parameters you cannot change unless you change the airport/runway.
|
||||
@@ -83,7 +89,7 @@ Next you can see a table with fields where you can change values. Each row repre
|
||||
|
||||
1. `Heading change (deg)` - information on how the heading of the aircraft should change in relation to the previous one. A value of `0` means no change, so continue with the same heading. And e.g. `-90` means a left turn of 90 degrees, `60` means a right turn of 60 degrees, etc.
|
||||
2. `Distance (m)` - distance in meters from the previous waypoint to the present one.
|
||||
3. `Alt change (AGL ft)` - information on how much the altitude on this leg of the route will increase (in feet above ground.) This information depend of selected airplane.
|
||||
3. `Alt change (AGL ft)` - information on how much the altitude on this leg of the route will increase (in feet above ground.) This information depend of selected airplane. AGL is always in terms of the place you are starting from (not the waypoint place).
|
||||
|
||||
If you enter a value less than or equal to `0` for `Distance (m)` field this means that here the route is end and the next rows will not be included to the flight plan.
|
||||
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
<distance-m type="double">0</distance-m>
|
||||
<alt-change-agl-ft type="double">0</alt-change-agl-ft>
|
||||
</init-wpt>
|
||||
<wpts>
|
||||
<description type="string">Default route around the start location</description>
|
||||
<max-alt-agl type="double"></max-alt-agl>
|
||||
</wpts>
|
||||
<total>
|
||||
<distance>0</distance>
|
||||
<alt>0</alt>
|
||||
|
||||
@@ -49,6 +49,7 @@ var loadExtraNasalFiles = func (addon) {
|
||||
"nasal/dialogs/thermal",
|
||||
"nasal/flight-plan",
|
||||
"nasal/scenario",
|
||||
"nasal/io/waypoint",
|
||||
"nasal/io/flight-plan-writer",
|
||||
"nasal/aerotow",
|
||||
"aerotow",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<addon>
|
||||
<identifier type="string">org.flightgear.addons.Aerotow</identifier>
|
||||
<name type="string">Aerotow Everywhere</name>
|
||||
<version type="string">1.6.0</version>
|
||||
<version type="string">1.8.0</version>
|
||||
|
||||
<authors>
|
||||
<author>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<hrule/>
|
||||
|
||||
<text>
|
||||
<label>Aerotow Everywhere version 1.6.0 - 23nd August 2022</label>
|
||||
<label>Aerotow Everywhere version 1.8.0 - 9th September 2022</label>
|
||||
</text>
|
||||
|
||||
<text>
|
||||
|
||||
@@ -75,14 +75,14 @@
|
||||
<row>0</row>
|
||||
<col>0</col>
|
||||
<halign>right</halign>
|
||||
<label>Aerotow</label>
|
||||
<label>Aerotow:</label>
|
||||
</text>
|
||||
|
||||
<combo>
|
||||
<row>0</row>
|
||||
<col>1</col>
|
||||
<halign>left</halign>
|
||||
<pref-width>150</pref-width>
|
||||
<pref-width>120</pref-width>
|
||||
<live>true</live>
|
||||
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/route/ai-model</property>
|
||||
<value>Piper J3 Cub</value>
|
||||
@@ -93,6 +93,24 @@
|
||||
<command>dialog-apply</command>
|
||||
</binding>
|
||||
</combo>
|
||||
|
||||
<text>
|
||||
<row>0</row>
|
||||
<col>2</col>
|
||||
<halign>right</halign>
|
||||
<label>Max alt (AGL):</label>
|
||||
</text>
|
||||
|
||||
<input>
|
||||
<row>0</row>
|
||||
<col>3</col>
|
||||
<halign>left</halign>
|
||||
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/route/wpts/max-alt-agl</property>
|
||||
<live>true</live>
|
||||
<binding>
|
||||
<command>dialog-apply</command>
|
||||
</binding>
|
||||
</input>
|
||||
</group>
|
||||
|
||||
<hrule/>
|
||||
@@ -501,7 +519,7 @@
|
||||
|
||||
<input>
|
||||
<halign>fill</halign>
|
||||
<halign>left</halign>
|
||||
<!-- <halign>left</halign> -->
|
||||
<property>/addons/by-id/org.flightgear.addons.Aerotow/addon-devel/route/wpts/description</property>
|
||||
<live>true</live>
|
||||
<binding>
|
||||
|
||||
@@ -39,6 +39,11 @@ var RouteDialog = {
|
||||
obj.calculateAltChangeAndTotals();
|
||||
}));
|
||||
|
||||
# Set listener for Max altitude AGL value in route dialog for recalculate altitude change
|
||||
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/route/wpts/max-alt-agl", func () {
|
||||
obj.calculateAltChangeAndTotals();
|
||||
}));
|
||||
|
||||
# Set listeners for distance fields for calculate altitude change
|
||||
for (var i = 0; i < obj.maxRouteWaypoints; i += 1) {
|
||||
append(obj.listeners, setlistener(obj.addonNodePath ~ "/addon-devel/route/wpts/wpt[" ~ i ~ "]/distance-m", func () {
|
||||
@@ -65,17 +70,26 @@ var RouteDialog = {
|
||||
var totalDistance = 0.0;
|
||||
var totalAlt = 0.0;
|
||||
var isEnd = false;
|
||||
var isAltLimit = false;
|
||||
|
||||
var isRouteMode = true;
|
||||
var aircraft = Aircraft.getSelected(me.addon, isRouteMode);
|
||||
|
||||
# 0 means without altitude limits
|
||||
var maxAltAgl = getprop(me.addonNodePath ~ "/addon-devel/route/wpts/max-alt-agl") or 0;
|
||||
|
||||
for (var i = 0; i < me.maxRouteWaypoints; i += 1) {
|
||||
var distance = getprop(me.addonNodePath ~ "/addon-devel/route/wpts/wpt[" ~ i ~ "]/distance-m");
|
||||
if (distance == nil) {
|
||||
break;
|
||||
var distance = getprop(me.addonNodePath ~ "/addon-devel/route/wpts/wpt[" ~ i ~ "]/distance-m") or 0;
|
||||
|
||||
# If we have reached the altitude limit, the altitude no longer changes (0)
|
||||
var altChange = isAltLimit ? 0 : aircraft.getAltChange(distance);
|
||||
if (maxAltAgl > 0 and altChange > 0 and totalAlt + altChange > maxAltAgl) {
|
||||
# We will exceed the altitude limit, so set the altChange to the altitude limit
|
||||
# and set isAltLimit flag that the limit is reached.
|
||||
altChange = maxAltAgl - totalAlt;
|
||||
isAltLimit = true;
|
||||
}
|
||||
|
||||
var altChange = aircraft.getAltChange(distance);
|
||||
setprop(me.addonNodePath ~ "/addon-devel/route/wpts/wpt[" ~ i ~ "]/alt-change-agl-ft", altChange);
|
||||
|
||||
if (!isEnd) {
|
||||
|
||||
@@ -36,7 +36,6 @@ var FlightPlan = {
|
||||
|
||||
obj.addonNodePath = addon.node.getPath();
|
||||
|
||||
obj.wptCount = 0;
|
||||
obj.coord = nil; # Coordinates for flight plan
|
||||
obj.heading = nil; # AI plane heading
|
||||
obj.altitude = nil; # AI plane altitude
|
||||
@@ -47,7 +46,7 @@ var FlightPlan = {
|
||||
#
|
||||
# Get inital location of glider.
|
||||
#
|
||||
# Return object with "lat", "lon" and 'heading".
|
||||
# Return object with "lat", "lon" and "heading".
|
||||
#
|
||||
getLocation: func () {
|
||||
var icao = getprop("/sim/airport/closest-airport-id");
|
||||
@@ -70,10 +69,11 @@ var FlightPlan = {
|
||||
if (rwyResult.distance > FlightPlan.MAX_RUNWAY_DISTANCE) {
|
||||
# The runway is too far away, we assume a bush start
|
||||
return {
|
||||
"type" : "bush",
|
||||
"lat" : gliderCoord.lat(),
|
||||
"lon" : gliderCoord.lon(),
|
||||
"heading" : getprop("/orientation/heading-deg"),
|
||||
"type" : "bush",
|
||||
"lat" : gliderCoord.lat(),
|
||||
"lon" : gliderCoord.lon(),
|
||||
"heading" : getprop("/orientation/heading-deg"),
|
||||
"elevation": me.getEleveationInFt(gliderCoord),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,10 +89,14 @@ var FlightPlan = {
|
||||
}
|
||||
|
||||
return {
|
||||
"type" : "runway",
|
||||
"lat" : rwyResult.runway.lat,
|
||||
"lon" : rwyResult.runway.lon,
|
||||
"heading" : rwyResult.runway.heading,
|
||||
"type" : "runway",
|
||||
"lat" : rwyResult.runway.lat,
|
||||
"lon" : rwyResult.runway.lon,
|
||||
"heading" : rwyResult.runway.heading,
|
||||
"elevation": me.getEleveationInFt(
|
||||
geo.Coord.new().set_latlon(rwyResult.runway.lat, rwyResult.runway.lon)
|
||||
),
|
||||
"length" : rwyResult.runway.length,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -137,6 +141,9 @@ var FlightPlan = {
|
||||
var isGliderPos = false;
|
||||
me.initAircraftVariable(location, isGliderPos);
|
||||
|
||||
# Max altitude without limits
|
||||
setprop(me.addonNodePath ~ "/addon-devel/route/wpts/max-alt-agl", 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);
|
||||
@@ -146,11 +153,11 @@ var FlightPlan = {
|
||||
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": 6000, "altChange": aircraft.vs * 6},
|
||||
{"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": -90, "dist": 6000, "altChange": aircraft.vs * 6},
|
||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||
{"hdgChange": 0, "dist": 0, "altChange": 0},
|
||||
@@ -161,20 +168,20 @@ var FlightPlan = {
|
||||
# 1 - 1st waypoint
|
||||
# 2 - 2nd waypoint, etc.
|
||||
#
|
||||
# 2 . . 1 7
|
||||
# . . .
|
||||
# . . .
|
||||
# 3 . .
|
||||
# 2 . . 1 5
|
||||
# . . .
|
||||
# . . .
|
||||
# . . .
|
||||
# . . .
|
||||
# . . .
|
||||
# . . .
|
||||
# . ^ 6
|
||||
# . . .
|
||||
# . . .
|
||||
# . . .
|
||||
# . ^ .
|
||||
# . .
|
||||
# . .
|
||||
# 4 . . . . 5
|
||||
# 3 . . . . 4
|
||||
|
||||
var index = 0;
|
||||
foreach (var wpt; wptData) {
|
||||
@@ -199,8 +206,6 @@ var FlightPlan = {
|
||||
# Return true on successful, otherwise false.
|
||||
#
|
||||
generateXml: func () {
|
||||
me.wptCount = 0;
|
||||
|
||||
var location = me.getLocation();
|
||||
if (location == nil) {
|
||||
return false;
|
||||
@@ -215,7 +220,7 @@ var FlightPlan = {
|
||||
|
||||
# Start at 2 o'clock from the glider...
|
||||
# Inital ktas must be >= 1.0
|
||||
me.addWptGround({"hdgChange": 60, "dist": 25}, {"altChange": 0, "ktas": 5});
|
||||
me.addWptGround({"shift": {"hdgChange": 60, "dist": 25, "altChange": 0}, "ktas": 5}); # 1
|
||||
|
||||
# Reset coord and heading
|
||||
isGliderPos = false;
|
||||
@@ -224,47 +229,140 @@ var FlightPlan = {
|
||||
var gliderOffsetM = me.getGliderOffsetFromRunwayThreshold(location);
|
||||
|
||||
# ... and line up with the runway
|
||||
me.addWptGround({"hdgChange": 0, "dist": me.getInitialDistance() + gliderOffsetM}, {"altChange": 0, "ktas": 2.5});
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": me.getInitialDistance() + gliderOffsetM, "altChange": 0}, "ktas": 2.5}); # 2
|
||||
|
||||
# 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});
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10, "altChange": 0}, "ktas": 5}); # 3
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 20, "altChange": 0}, "ktas": 5}); # 4
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 20, "altChange": 0}, "ktas": aircraft.speed / 6}); # 5
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10, "altChange": 0}, "ktas": aircraft.speed / 5}); # 6
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 4}); # 7
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 3.5}); # 8
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 3}); # 9
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 2.5}); # 10
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 2}); # 11
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 1.75}); # 12
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 1.5}); # 13
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed / 1.25}); # 14
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 10 * aircraft.rolling, "altChange": 0}, "ktas": aircraft.speed}); # 15
|
||||
|
||||
# Take-off
|
||||
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});
|
||||
me.addWptAir({ "shift": {"hdgChange": 0, "dist": 100 * aircraft.rolling, "elevation": 3}, "ktas": aircraft.speed * 1.05}); # 16
|
||||
me.addWptAir({ "shift": {"hdgChange": 0, "dist": 100, "altChange": aircraft.vs / 10}, "ktas": aircraft.speed * 1.025}); # 17
|
||||
|
||||
|
||||
# 0 means without altitude limits
|
||||
var maxAltAgl = getprop(me.addonNodePath ~ "/addon-devel/route/wpts/max-alt-agl") or 0;
|
||||
var totalAlt = 0.0;
|
||||
var isAltLimit = false;
|
||||
|
||||
# Add waypoints according to user settings
|
||||
var speedInc = 1.0;
|
||||
foreach (var wptNode; props.globals.getNode(me.addonNodePath ~ "/addon-devel/route/wpts").getChildren("wpt")) {
|
||||
var dist = wptNode.getChild("distance-m").getValue();
|
||||
if (dist <= 0.0) {
|
||||
var distance = wptNode.getChild("distance-m").getValue();
|
||||
if (distance <= 0.0) {
|
||||
break;
|
||||
}
|
||||
|
||||
var hdgChange = wptNode.getChild("heading-change").getValue();
|
||||
var altChange = aircraft.getAltChange(dist);
|
||||
|
||||
speedInc += ((dist / Aircraft.DISTANCE_DETERMINANT) * 0.025);
|
||||
# If we have reached the altitude limit, the altitude no longer changes (0)
|
||||
var altChange = isAltLimit ? 0 : aircraft.getAltChange(distance);
|
||||
if (maxAltAgl > 0 and altChange > 0 and totalAlt + altChange > maxAltAgl) {
|
||||
# We will exceed the altitude limit, so set the altChange to the altitude limit
|
||||
# and set isAltLimit flag that the limit is reached.
|
||||
altChange = maxAltAgl - totalAlt;
|
||||
isAltLimit = true;
|
||||
}
|
||||
|
||||
speedInc += ((distance / 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});
|
||||
totalAlt += altChange;
|
||||
|
||||
me.addWptAir({"shift": {"hdgChange": hdgChange, "dist": distance, "altChange": altChange}, "ktas": ktas});
|
||||
}
|
||||
|
||||
me.addWptEnd();
|
||||
# Back to airport if possible
|
||||
if (location.type == "runway") {
|
||||
# Add extra near waypoint to keep plane in whole designed track
|
||||
me.addWptAir({"shift": {"hdgChange": hdgChange, "dist": 100, "altChange": altChange}, "ktas": ktas});
|
||||
|
||||
var coordRwyThreshold = geo.Coord.new().set_latlon(location.lat, location.lon);;
|
||||
|
||||
# Check distance to runway threshold
|
||||
var dostanceToThreshold = me.coord.distance_to(coordRwyThreshold);
|
||||
|
||||
# Reset variables
|
||||
me.heading = location.heading; # runway heading
|
||||
me.coord = coordRwyThreshold;
|
||||
|
||||
# Move o the left of the runway threshold
|
||||
me.heading = me.correctHeading(me.heading - 90);
|
||||
me.coord.apply_course_distance(me.heading, 1000);
|
||||
|
||||
# Add a waypoint to the left of the runway + 3000 m to the middle of length
|
||||
# Descend as far as you can to max elevation + 3000 ft
|
||||
var halfRwyLenght = location.length / 2;
|
||||
var altAgl = me.altitude - location.elevation;
|
||||
var elevation = altAgl - (aircraft.getAltChange(dostanceToThreshold) * 2);
|
||||
if (elevation < 3000) {
|
||||
elevation = 3000;
|
||||
}
|
||||
me.addWptAir({"shift": {"hdgChange": 90, "dist": halfRwyLenght, "elevation": elevation}, "ktas": aircraft.speed});
|
||||
|
||||
# Fly downwind away of threshold, how far depend of the altitude
|
||||
var desiredElevation = 2000;
|
||||
var distance = (((elevation - desiredElevation) / (aircraft.vs * 3)) * 1000);
|
||||
if (distance < 3000) {
|
||||
distance = 3000;
|
||||
}
|
||||
me.addWptAir({"shift": {"hdgChange": -180, "dist": halfRwyLenght + distance, "elevation": desiredElevation}, "ktas": aircraft.speed});
|
||||
|
||||
# Turn to base leg
|
||||
me.addWptAir({"shift": {"hdgChange": -90, "dist": 1000, "elevation": 1000}, "ktas": aircraft.speed, "flapsDown": true});
|
||||
|
||||
# Reset variables
|
||||
me.coord = geo.Coord.new().set_latlon(location.lat, location.lon); # runway threshold
|
||||
|
||||
# Turn on final
|
||||
me.addWptAir({
|
||||
"coord" : me.coord,
|
||||
"crossAt" : location.elevation + 10,
|
||||
"ktas" : aircraft.speed * 0.75,
|
||||
"flapsDown": true,
|
||||
"gearDown" : true,
|
||||
});
|
||||
|
||||
# Reset variables
|
||||
me.heading = location.heading;
|
||||
|
||||
# Flare
|
||||
me.addWptAir({
|
||||
"shift" : {"hdgChange": 0, "dist": 100, "elevation": 10},
|
||||
"ktas" : aircraft.speed * 0.7,
|
||||
"flapsDown": true,
|
||||
"gearDown" : true,
|
||||
});
|
||||
|
||||
# Touchdown
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 200, "elevation": 0}, "ktas": aircraft.speed * 0.6});
|
||||
|
||||
# Break
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 200, "elevation": 0}, "ktas": aircraft.speed * 0.4});
|
||||
|
||||
# Slow down to 5 kt
|
||||
me.addWptGround({"shift": {"hdgChange": 0, "dist": 50, "elevation": 0}, "ktas": 5});
|
||||
|
||||
# Turn right out of runway and full stop
|
||||
me.addWptEnd({"shift": {"hdgChange": 90, "dist": 100, "elevation": 0}, "ktas": 0, "onGround": true});
|
||||
}
|
||||
else {
|
||||
me.addWptEnd();
|
||||
}
|
||||
|
||||
me.flightPlanWriter.close();
|
||||
|
||||
@@ -325,26 +423,24 @@ var FlightPlan = {
|
||||
#
|
||||
# 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.
|
||||
# }
|
||||
# wptData - hash object with waypoint data
|
||||
#
|
||||
addWptGround: func (coordOffset, performance) {
|
||||
me.wrireWpt(nil, coordOffset, performance, "ground");
|
||||
addWptGround: func (wptData) {
|
||||
wptData.onGround = true;
|
||||
me.addWpt(wptData);
|
||||
},
|
||||
|
||||
#
|
||||
# Add new waypoint in air
|
||||
#
|
||||
addWptAir: func (coordOffset, performance) {
|
||||
me.wrireWpt(nil, coordOffset, performance, "air");
|
||||
# wptData - hash object wtih waypoint data
|
||||
#
|
||||
addWptAir: func (wptData) {
|
||||
if (contains(wptData, "onGround")) {
|
||||
wptData.onGround = nil;
|
||||
}
|
||||
|
||||
me.addWpt(wptData);
|
||||
},
|
||||
|
||||
#
|
||||
@@ -353,70 +449,115 @@ var FlightPlan = {
|
||||
# sec - Number of seconds for wait
|
||||
#
|
||||
addWptWait: func (sec) {
|
||||
me.wrireWpt("WAIT", {}, {}, nil, sec);
|
||||
me.addWpt({"waitSec": sec});
|
||||
},
|
||||
|
||||
#
|
||||
# Add "END" waypoint
|
||||
# Add "END" waypoint with optional waypoint data
|
||||
#
|
||||
addWptEnd: func () {
|
||||
me.wrireWpt("END", {}, {});
|
||||
addWptEnd: func (wptData = nil) {
|
||||
if (wptData == nil) {
|
||||
wptData = {};
|
||||
}
|
||||
|
||||
wptData.name = "END";
|
||||
|
||||
me.addWpt(wptData);
|
||||
},
|
||||
|
||||
#
|
||||
# Write waypoint to flight plan file
|
||||
# Add new waypoint with given waypoint data
|
||||
#
|
||||
# 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
|
||||
# wptData = {
|
||||
# shift: { - Optionally hash with data to calculate next coordinates (lat, lon) and altitude of waypoint
|
||||
# hdgChange - How the aircraft's heading supposed to change? 0 - keep the same heading.
|
||||
# dist - Distance in meters to calculate next waypoint coordinates.
|
||||
# altChange - How the aircraft's altitude is supposed to change? 0 - keep the same altitude.
|
||||
# elevation - 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.
|
||||
# },
|
||||
# coord - The geo.Coord object, required if shift is not given
|
||||
# crossAt - Altitude in feet, required if shift is not given
|
||||
# ktas - True airspeed in knots, required
|
||||
# onGround - If true then set on ground, otherwise set in air
|
||||
# flapsDown - If true then set flaps down, otherwise set flaps up
|
||||
# gearDown - If true then set gear down, otherwise set gear up
|
||||
# waitSec - Number of seconds for WIAT waypoint
|
||||
# }
|
||||
#
|
||||
wrireWpt: func (
|
||||
name,
|
||||
coordOffset,
|
||||
performance,
|
||||
groundAir = nil,
|
||||
sec = nil
|
||||
) {
|
||||
var coord = nil;
|
||||
if (contains(coordOffset, "hdgChange") and contains(coordOffset, "dist")) {
|
||||
me.heading += coordOffset.hdgChange;
|
||||
if (me.heading < 0) {
|
||||
me.heading += 360;
|
||||
}
|
||||
addWpt: func (wptData) {
|
||||
if (contains(wptData, "shift")) {
|
||||
me.shiftWpt(wptData.shift);
|
||||
|
||||
if (me.heading > 360) {
|
||||
me.heading -= 360;
|
||||
}
|
||||
|
||||
me.coord.apply_course_distance(me.heading, coordOffset.dist);
|
||||
coord = me.coord;
|
||||
# Set coord and crossAt updated by shiftWpt()
|
||||
wptData.coord = me.coord;
|
||||
wptData.crossAt = me.altitude;
|
||||
}
|
||||
|
||||
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 += performance.altChange;
|
||||
alt = me.altitude;
|
||||
var wpt = Waypoint.new().setHashData(wptData);
|
||||
|
||||
me.flightPlanWriter.write(wpt);
|
||||
},
|
||||
|
||||
#
|
||||
# Calculate heading, coordinates and altitude from data in wptShift hash
|
||||
#
|
||||
# wptShift - hash object with data
|
||||
#
|
||||
shiftWpt: func (wptShift) {
|
||||
if (!contains(wptShift, "hdgChange")) {
|
||||
die("ERROR aerotow add-on: missing 'hdgChange' for computeWptShift");
|
||||
}
|
||||
|
||||
var ktas = contains(performance, "ktas") ? performance.ktas : nil;
|
||||
if (!contains(wptShift, "dist")) {
|
||||
die("ERROR aerotow add-on: missing 'dist' for computeWptShift");
|
||||
}
|
||||
|
||||
name = name == nil ? me.wptCount : name;
|
||||
me.flightPlanWriter.write(name, coord, alt, ktas, groundAir, sec);
|
||||
# Shift heading and coordinates
|
||||
me.heading = me.correctHeading(me.heading + wptShift.hdgChange);
|
||||
me.coord.apply_course_distance(me.heading, wptShift.dist);
|
||||
|
||||
me.wptCount += 1;
|
||||
if (contains(wptShift, "elevation")) {
|
||||
# Set the altitude as the elevation for coordinates of the point plus the given elevation
|
||||
me.altitude = me.getEleveationInFt(me.coord) + wptShift.elevation;
|
||||
return;
|
||||
}
|
||||
|
||||
if (contains(wptShift, "altChange")) {
|
||||
# Change altitude by given altChange
|
||||
me.altitude += wptShift.altChange;
|
||||
}
|
||||
},
|
||||
|
||||
#
|
||||
# Correct the heading value that to be from 0 to 360
|
||||
#
|
||||
# heading - heading for correction
|
||||
# Return heading in range from 0 to 360.
|
||||
#
|
||||
correctHeading: func (heading) {
|
||||
if (heading < 0) {
|
||||
heading += 360;
|
||||
}
|
||||
|
||||
if (heading > 360) {
|
||||
heading -= 360;
|
||||
}
|
||||
|
||||
return heading;
|
||||
},
|
||||
|
||||
#
|
||||
# Get elevation of given coordinates in feet
|
||||
#
|
||||
# coord - geo.Coord object
|
||||
#
|
||||
getEleveationInFt: func (coord) {
|
||||
if (coord == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return geo.elevation(coord.lat(), coord.lon()) * globals.M2FT;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ var FlightPlanWriter = {
|
||||
|
||||
obj.fpFileHandler = nil; # Handler for wrire flight plan to file
|
||||
obj.flightPlanPath = addon.storagePath ~ "/AI/FlightPlans/" ~ FlightPlan.FILENAME_FLIGHTPLAN;
|
||||
obj.wptCount = 1;
|
||||
|
||||
return obj;
|
||||
},
|
||||
@@ -31,6 +32,8 @@ var FlightPlanWriter = {
|
||||
# Open XML file to wrire flight plan
|
||||
#
|
||||
open: func () {
|
||||
me.wptCount = 1;
|
||||
|
||||
me.fpFileHandler = io.open(me.flightPlanPath, "w");
|
||||
|
||||
if (me.fpFileHandler) {
|
||||
@@ -47,50 +50,61 @@ var FlightPlanWriter = {
|
||||
#
|
||||
# Write single waypoint to XML file with flight plan
|
||||
#
|
||||
# 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
|
||||
# wpt - Waypoint object
|
||||
#
|
||||
write: func (name, coord = nil, alt = nil, ktas = nil, groundAir = nil, sec = nil) {
|
||||
write: func (wpt) {
|
||||
if (!me.fpFileHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
var str = " <wpt>\n"
|
||||
~ " <name>" ~ name ~ "</name>\n";
|
||||
if (wpt.name == nil) {
|
||||
wpt.setName(me.wptCount);
|
||||
}
|
||||
|
||||
if (coord != nil) {
|
||||
str ~= " <lat>" ~ coord.lat() ~ "</lat>\n";
|
||||
str ~= " <lon>" ~ coord.lon() ~ "</lon>\n";
|
||||
var str = " <wpt>\n"
|
||||
~ " <name>" ~ wpt.name ~ "</name>\n";
|
||||
|
||||
if (wpt.coord != nil) {
|
||||
str ~= " <lat>" ~ wpt.coord.lat() ~ "</lat>\n";
|
||||
str ~= " <lon>" ~ wpt.coord.lon() ~ "</lon>\n";
|
||||
str ~= " <!--\n"
|
||||
~ " " ~ coord.lat() ~ "," ~ coord.lon() ~ "\n"
|
||||
~ " " ~ wpt.coord.lat() ~ "," ~ wpt.coord.lon() ~ "\n"
|
||||
~ " -->\n";
|
||||
}
|
||||
|
||||
if (alt != nil) {
|
||||
# str ~= " <alt>" ~ alt ~ "</alt>\n";
|
||||
str ~= " <crossat>" ~ alt ~ "</crossat>\n";
|
||||
if (wpt.alt != nil) {
|
||||
str ~= " <alt>" ~ wpt.alt ~ "</alt>\n";
|
||||
}
|
||||
|
||||
if (ktas != nil) {
|
||||
str ~= " <ktas>" ~ ktas ~ "</ktas>\n";
|
||||
if (wpt.crossAt != nil) {
|
||||
str ~= " <crossat>" ~ wpt.crossAt ~ "</crossat>\n";
|
||||
}
|
||||
|
||||
if (groundAir != nil) {
|
||||
var onGround = groundAir == "ground" ? "true" : "false";
|
||||
str ~= " <on-ground>" ~ onGround ~ "</on-ground>\n";
|
||||
if (wpt.ktas != nil) {
|
||||
str ~= " <ktas>" ~ wpt.ktas ~ "</ktas>\n";
|
||||
}
|
||||
|
||||
if (sec != nil) {
|
||||
str ~= " <time-sec>" ~ sec ~ "</time-sec>\n";
|
||||
if (wpt.onGround != nil) {
|
||||
str ~= " <on-ground>" ~ (wpt.onGround ? "true" : "false") ~ "</on-ground>\n";
|
||||
}
|
||||
|
||||
if (wpt.waitSec != nil) {
|
||||
str ~= " <time-sec>" ~ wpt.waitSec ~ "</time-sec>\n";
|
||||
}
|
||||
|
||||
if (wpt.flapsDown != nil) {
|
||||
str ~= " <flaps-down>" ~ (wpt.flapsDown ? "true" : "false") ~ "</flaps-down>\n";
|
||||
}
|
||||
|
||||
if (wpt.gearDown != nil) {
|
||||
str ~= " <gear-down>" ~ (wpt.gearDown ? "true" : "false") ~ "</gear-down>\n";
|
||||
}
|
||||
|
||||
str ~= " </wpt>\n";
|
||||
|
||||
io.write(me.fpFileHandler, str);
|
||||
|
||||
me.wptCount += 1;
|
||||
},
|
||||
|
||||
#
|
||||
|
||||
175
nasal/io/waypoint.nas
Normal file
175
nasal/io/waypoint.nas
Normal file
@@ -0,0 +1,175 @@
|
||||
#
|
||||
# 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)
|
||||
#
|
||||
|
||||
#
|
||||
# Class Waypoint represent the data of waypoint in the flight plan.
|
||||
#
|
||||
var Waypoint = {
|
||||
#
|
||||
# Constructor
|
||||
#
|
||||
new: func () {
|
||||
var obj = { parents: [Waypoint] };
|
||||
|
||||
obj.name = nil; # Name of waypoint. Special names are: "WAIT", "END".
|
||||
obj.coord = nil; # geo.Coord object
|
||||
obj.alt = nil; # Altitude AMSL in feet
|
||||
obj.crossAt = nil; # Altitude AMSL in feet
|
||||
obj.ktas = nil; # True airspeed in knots
|
||||
obj.onGround = nil; # If true then on the ground, otherwise in air
|
||||
obj.flapsDown = nil; # If true then flaps down, otherwise up
|
||||
obj.gearDown = nil; # If true then gear down, otherwise up
|
||||
obj.waitSec = nil; # Number of seconds for "WAIT" waypoint
|
||||
|
||||
return obj;
|
||||
},
|
||||
|
||||
#
|
||||
# Set name of waypoint
|
||||
#
|
||||
# name - Name of waypoint
|
||||
#
|
||||
setName: func (name) {
|
||||
me.name = name;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set coordinates of waypoint
|
||||
#
|
||||
# coord - geo.Coord object
|
||||
#
|
||||
setCoord: func (coord) {
|
||||
me.coord = coord;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set altitude in feet of waypoint
|
||||
#
|
||||
# alt - altitude in feet
|
||||
#
|
||||
setAlt: func (alt) {
|
||||
me.alt = alt;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set altitude in feet of waypoint as cross at
|
||||
#
|
||||
# alt - altitude in feet
|
||||
#
|
||||
setCrossAt: func (crossAt) {
|
||||
me.crossAt = crossAt;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set true airspeed in knots for waypoint
|
||||
#
|
||||
# ktas - true airspeed in knots
|
||||
#
|
||||
setKtas: func (ktas) {
|
||||
me.ktas = ktas;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set waypoint on the ground
|
||||
#
|
||||
setOnGround: func () {
|
||||
me.onGround = 1;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set flaps down
|
||||
#
|
||||
setFlapsDown: func () {
|
||||
me.flapsDown = 1;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set gear down
|
||||
#
|
||||
setGearDown: func () {
|
||||
me.gearDown = 1;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set number of seconds for WAIT waypoint. This force to use "WAIT" name.
|
||||
#
|
||||
# waitSec - Number of seconds
|
||||
#
|
||||
setWaitSec: func (waitSec) {
|
||||
me.setName("WAIT"); # force WAIT name
|
||||
me.waitSec = waitSec;
|
||||
|
||||
return me;
|
||||
},
|
||||
|
||||
#
|
||||
# Set all waypoint data from given hash object
|
||||
#
|
||||
setHashData: func (wptData) {
|
||||
if (wptData == nil) {
|
||||
return me;
|
||||
}
|
||||
|
||||
if (contains(wptData, "name")) {
|
||||
me.setName(wptData.name);
|
||||
}
|
||||
|
||||
if (contains(wptData, "coord")) {
|
||||
me.setCoord(wptData.coord);
|
||||
}
|
||||
|
||||
if (contains(wptData, "crossAt")) {
|
||||
me.setCrossAt(wptData.crossAt);
|
||||
}
|
||||
|
||||
if (contains(wptData, "alt")) {
|
||||
me.setAlt(wptData.alt);
|
||||
}
|
||||
|
||||
if (contains(wptData, "ktas")) {
|
||||
me.setKtas(wptData.ktas);
|
||||
}
|
||||
|
||||
if (contains(wptData, "onGround") and wptData.onGround) {
|
||||
me.setOnGround();
|
||||
}
|
||||
|
||||
if (contains(wptData, "flapsDown") and wptData.flapsDown) {
|
||||
me.setFlapsDown();
|
||||
}
|
||||
|
||||
if (contains(wptData, "gearDown") and wptData.gearDown) {
|
||||
me.setGearDown();
|
||||
}
|
||||
|
||||
if (contains(wptData, "waitSec")) {
|
||||
me.setWaitSec(wptData.waitSec);
|
||||
}
|
||||
|
||||
return me;
|
||||
}
|
||||
};
|
||||
@@ -84,7 +84,7 @@ var Scenario = {
|
||||
"class": "aerotow-dragger",
|
||||
"model": Aircraft.getSelected(me.addon).modelPath,
|
||||
"flightplan": FlightPlan.FILENAME_FLIGHTPLAN,
|
||||
"repeat": true, # start again indefinitely
|
||||
"repeat": true, # start again indefinitely, it will work if the aircraft stops on the ground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user