Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / jetways / jetways.nas
1 ###############################################################################
2 ##
3 ##  Animated Jetway System. Spawns and manages interactive jetway models.
4 ##
5 ##  Copyright (C) 2011  Ryan Miller
6 ##  This file is licensed under the GPL license version 2 or later.
7 ##
8 ###############################################################################
9
10 ###############################################################################
11 # (See http://wiki.flightgear.org/Howto:_Animated_jetways)
12 #
13 # Special jetway definition files located in $FG_ROOT/Airports/Jetways/XXXX.xml
14 # for each airport are loaded when the user's aircraft is within 50 nm of the
15 # airport. The script dynamically generates runtime model files, writes them to
16 # $FG_ROOT/Models/Airport/Jetway/runtimeX.xml, and places them into the
17 # simulator using the model manager.
18 #
19 # Different jetway models can be defined and are placed under
20 # $FG_ROOT/Models/Airport/Jetway/XXX.xml.
21 #
22 # Jetways can be extended/retracted independently either by user operation or
23 # by automatic extension for AI models and multiplayer aircraft.
24 #
25 # UTILITY FUNCTIONS
26 # -----------------
27 #
28 # print_debug(<message>)                        - prints debug messages
29 #       <message>                               - message to print
30 #
31 # print_error(<messsage>)                       - prints error messages
32 #       <message>                               - error to print
33 #
34 # alert(<message>)                              - displays an alert message in-sim
35 #       <message>                               - the message
36 #
37 # normdeg(<angle>)                              - normalizes angle measures between -180° and 180°
38 #       <angle>                                 - angle to normalize
39 #
40 # remove(<vector>, <item>)                      - removes an element from a vector
41 #       <vector>                                - vector
42 #       <index>                                 - item
43 #
44 # isin(<vector>, <item>)                        - checks if an item exists in a vector
45 #       <vector>                                - vector
46 #       <item>                                  - item
47 #
48 # putmodel(<path>, <lat>, <lon>, <alt>, <hdg>)  - add a model to the scene graph (unlike geo.put_model(), models added with this function can be adjusted)
49 #       <path>                                  - model path
50 #       <lat>                                   - latitude
51 #       <lon>                                   - longitude
52 #       <alt>                                   - altitude in m
53 #       <hdg>                                   - heading
54 #
55 # interpolate_table(<table>, <value>)           - interpolates a value within a table
56 #       <table>                                 - interpolation table/vector, in the format of [[<ind>, <dep>], [<ind>, <dep>], ... ]
57 #       <value>                                 - value
58 #
59 # get_relative_filepath(<path>, <target>)       - gets a relative file path from a directory
60 #       <path>                                  - directory path should be relative to
61 #       <target>                                - target directory
62 #
63 # find_airports(<max dist>)                     - gets a list of nearest airports
64 #       <max dist>                              - maximum search distance in nm (currently unused)
65 #
66 # JETWAY CLASS
67 # ------------
68 #
69 # Jetway.                                       - creates a new jetway object/model
70 #   new(<airport>, <model>, <gate>, <door>,
71 #       <airline>, <lat>, <lon>, <alt>,
72 #       <heading>, [, <init_extend>]
73 #       [, <init_heading>] [, <init_pitch>]
74 #       [, <init_ent_heading>])
75 #       <airport>                               - ICAO of associated airport
76 #       <model>                                 - jetway model definition (i.e. Models/Airport/Jetway/generic.xml)
77 #       <gate>                                  - gate number (i.e. "A1")
78 #       <door>                                  - door number (i.e. 0)
79 #       <airline>                               - airline code (i.e. "AAL")
80 #       <lat>                                   - latitude location of model
81 #       <lon>                                   - longitude location of model
82 #       <alt>                                   - elevation of model in m
83 #       <heading>                               - (optional) heading of model
84 #       <init_extend>                           - (optional) initial extension of tunnel in m
85 #       <init_heading>                          - (optional) initial rotation of tunnel along the Z axis
86 #       <init_pitch>                            - (optional) initial pitch of tunnel (rotation along Y axis)
87 #       <init_ent_heading>                      - (optional) initial rotation of entrance along the Z axis
88 #
89 #   toggle(<user>, <heading>, <coord>           - extends/retracts a jetway
90 #          [, <hood>])
91 #       <user>                                  - whether or not jetway is toggled by user command (0/1)
92 #       <heading>                               - heading of aircraft to connect to
93 #       <coord>                                 - a geo.Coord of the target aircraft's door
94 #       <hood>                                  - (optional) amount to rotate jetway hood (only required when <user> != 1)
95 #
96 #   extend(<user>, <heading>, <coord>           - extends a jetway (should be called by Jetway.toggle())
97 #          [, <hood>])
98 #       <user>                                  - whether or not jetway is toggled by user command (0/1)
99 #       <heading>                               - heading of aircraft to connect to
100 #       <coord>                                 - a geo.Coord of the target aircraft's door
101 #       <hood>                                  - (optional) amount to rotate jetway hood (only required when <user> != 1)
102 #
103 #   retract(<user>)                             - retracts a jetway (should be called by Jetway.toggle())
104 #       <user>                                  - whether or not a jetway is toggled by user command (0/1)
105 #
106 #   remove()                                    - removes a jetway object and its model
107 #
108 #   reload()                                    - reloads a jetway object and its model
109 #
110 #   setpos(<lat>, <lon>, <heading>, <alt>)      - moves a jetway to a new location
111 #       <lat>                                   - new latitude
112 #       <lon>                                   - new longitude
113 #       <heading>                               - new heading
114 #       <alt>                                   - new altitude in m
115 #
116 #   setmodel(<model>, <airline>, <gate>)        - changes the jetway model
117 #       <model>                                 - new model
118 #       <airline>                               - new airline sign code
119 #       <gate>                                  - new gate number
120 #
121 # INTERACTION FUNCTIONS
122 # ---------------------
123 #
124 # dialog()                                      - open settings dialog
125 #
126 # toggle_jetway(<id>)                           - toggles a jetway by user command (should be called by a pick animation in a jetway model)
127 #       <id>                                    - id number of jetway to toggle
128 #
129 # toggle_jetway_from_coord(<door>, <hood>,      - toggles a jetway with the target door at the specified coordinates
130 #                          <heading>, [<lat>,
131 #                          <lon>] [<coord>])
132 #       <door>                                  - door number (i.e. 0)
133 #       <hood>                                  - amount to rotate jetway hood
134 #       <lat>                                   - (required or <coord>) latitude location of door
135 #       <lon>                                   - (required or <coord>) longitude location of door
136 #       <coord>                                 - (required or <lat>, <lon>) a geo.Coord of the door
137 #
138 # toggle_jetway_from_model(<model node>)        - toggles a jetway using an AI model instead of the user's aircraft
139 #       <model node>                            - path of AI model (i.e. /ai/models/aircraft[0])- can be the path in a string or a props.Node
140 #
141 # INTERNAL FUNCTIONS
142 # ------------------
143 #
144 # load_airport_jetways(<airport>)               - loads jetways at an airport
145 #       <airport>                               - ICAO of airport
146 #
147 # unload_airport_jetways(<airport>)             - unloads jetways at an airport
148 #       <airport>                               - ICAO of airport
149 #
150 # update_jetways()                              - interpolates model animation values
151 #
152 # load_jetways()                                - loads new jetway models and unloads out-of-range models every 10 seconds; also connects AI and MP aircraft
153 #
154
155 ## Utility functions
156 ####################
157
158 # prints debug messages
159 var print_debug = func(msg)
160  {
161  if (debug_switch.getBoolValue())
162   {
163   print(msg);
164   }
165  };
166 # prints error messages
167 var print_error = func(msg)
168  {
169  print("\x1b[31m" ~ msg ~ "\x1b[m");
170  };
171 # alerts the user
172 var alert = func(msg)
173  {
174  setprop("/sim/messages/ground", msg);
175  };
176 # normalizes headings between -180 and 180
177 var normdeg = func(x)
178  {
179  while (x >= 180)
180   {
181   x -= 360;
182   }
183  while (x <= -180)
184   {
185   x += 360;
186   }
187  return x;
188  };
189 # deletes an item in a vector
190 var remove = func(vector, item)
191  {
192  var s = size(vector);
193  var found = 0;
194  for (var i = 0; i < s; i += 1)
195   {
196   if (found)
197    {
198    vector[i - 1] = vector[i];
199    }
200   elsif (vector[i] == item)
201    {
202    found = 1;
203    }
204   }
205  if (found) setsize(vector, s - 1);
206  return vector;
207  };
208 # checks if an item is in a vector
209 var isin = func(vector, v)
210  {
211  foreach (var item; vector)
212   {
213   if (item == v) return 1;
214   }
215  return 0;
216  };
217 # adds a model
218 var putmodel = func(path, lat, lon, alt, hdg)
219  {
220  var models = props.globals.getNode("/models");
221  var model = nil;
222  for (var i = 0; 1; i += 1)
223   {
224   if (models.getChild("model", i, 0) == nil)
225    {
226    model = models.getChild("model", i, 1);
227    break;
228    }
229   }
230  var model_path = model.getPath();
231  model.getNode("path", 1).setValue(path);
232  model.getNode("latitude-deg", 1).setDoubleValue(lat);
233  model.getNode("latitude-deg-prop", 1).setValue(model_path ~ "/latitude-deg");
234  model.getNode("longitude-deg", 1).setDoubleValue(lon);
235  model.getNode("longitude-deg-prop", 1).setValue(model_path ~ "/longitude-deg");
236  model.getNode("elevation-ft", 1).setDoubleValue(alt * M2FT);
237  model.getNode("elevation-ft-prop", 1).setValue(model_path ~ "/elevation-ft");
238  model.getNode("heading-deg", 1).setDoubleValue(hdg);
239  model.getNode("heading-deg-prop", 1).setValue(model_path ~ "/heading-deg");
240  model.getNode("pitch-deg", 1).setDoubleValue(0);
241  model.getNode("pitch-deg-prop", 1).setValue(model_path ~ "/pitch-deg");
242  model.getNode("roll-deg", 1).setDoubleValue(0);
243  model.getNode("roll-deg-prop", 1).setValue(model_path ~ "/roll-deg");
244  model.getNode("load", 1).remove();
245  return model;
246  };
247
248 # interpolates a value
249 var interpolate_table = func(table, v)
250  {
251  var x = 0;
252  forindex (i; table)
253   {
254   if (v >= table[i][0])
255    {
256    x = i + 1 < size(table) ? (v - table[i][0]) / (table[i + 1][0] - table[i][0]) * (table[i + 1][1] - table[i][1]) + table[i][1] : table[i][1];
257    }
258   }
259  return x;
260  };
261 # gets a relative file path
262 var get_relative_filepath = func(path, target)
263  {
264  var newpath = "";
265  for (var i = size(path) - 1; i >= 0; i -= 1)
266   {
267   var char = substr(path, i, 1);
268   if (char == "/") newpath ~= "../";
269   }
270  # we can just append the target path for UNIX systems, but we need to remove the drive letter prefix for DOS systems
271  return newpath ~ (string.match(substr(target, 0, 3), "?:/") ? substr(target, 2, size(target) - 2) : target);
272  };
273 # gets a list of nearest airports
274 # TODO: Don't use /sim/airport/nearest-airport-id, which restricts the list to 1 airport
275 var find_airports = func(max_distance)
276  {
277  var apt = getprop("/sim/airport/closest-airport-id");
278  return apt == "" ? nil : [apt];
279  };
280
281 ## Global variables
282 ###################
283
284 var root = nil;
285 var home = nil;
286 var scenery = [];
287
288 var UPDATE_PERIOD = 0;
289 var LOAD_PERIOD = 10;
290 var LOAD_DISTANCE = 50;                         # in nautical miles
291 var LOAD_JETWAY_PERIOD = 0.05;
292 var NUMBER_OF_JETWAYS = 1000;                   # approx max number of jetways loadable in FG
293 var runtime_files = NUMBER_OF_JETWAYS / LOAD_PERIOD * LOAD_JETWAY_PERIOD;
294 runtime_files = int(runtime_files) == runtime_files ? runtime_files : int(runtime_files) + 1;
295 var runtime_file = 0;
296 var update_loopid = -1;
297 var load_loopid = -1;
298 var load_listenerid = nil;
299 var loadids = {};
300 var dialog_object = nil;
301 var loaded_airports = [];
302 var jetways = [];
303
304 # properties
305 var on_switch = nil;
306 var debug_switch = nil;
307 var mp_switch = nil;
308 var jetway_id_prop = "/sim/jetways/last-loaded-jetway";
309
310 # interpolation tables
311 var extend_rate = 0.5;
312 var extend_table = [
313   [0.0, 0.0],
314   [0.2, 0.3],
315   [0.6, 0.3],
316   [0.8, 1.0],
317   [1.0, 1.0]
318  ];
319 var pitch_rate = 1;
320 var pitch_table = [
321   [0.0, 0.0],
322   [0.4, 0.7],
323   [0.7, 1.0],
324   [1.0, 1.0]
325  ];
326 var heading_rate = 1;
327 var heading_table = [
328   [0.0, 0.0],
329   [0.2, 0.0],
330   [0.6, 0.7],
331   [0.9, 1.0],
332   [1.0, 1.0]
333  ];
334 var heading_entrance_rate = 5;
335 var heading_entrance_table = [
336   [0.0, 0.0],
337   [0.3, 0.0],
338   [0.6, 0.7],
339   [0.8, 1.0],
340   [1.0, 1.0]
341  ];
342 var hood_rate = 1;
343 var hood_table = [
344   [0.0, 0.0],
345   [0.9, 0.0],
346   [1.0, 1.0]
347  ];
348
349 ## Classes
350 ##########
351
352 # main jetway class
353 var Jetway =
354  {
355  new: func(airport, model, gate, door, airline, lat, lon, alt, heading, init_extend = 0, init_heading = 0, init_pitch = 0, init_ent_heading = 0)
356   {
357   var id = 0;
358   for (var i = 0; 1; i += 1)
359    {
360    if (i == size(jetways))
361     {
362     setsize(jetways, i + 1);
363     id = i;
364     break;
365     }
366    elsif (jetways[i] == nil)
367     {
368     id = i;
369     }
370    }
371   # locate the jetway model directory and load the model tree
372   var model_tree = nil;
373   var model_file = "";
374   var model_dir = "";
375   var airline_file = "";
376   # search in scenery directories
377   foreach (var scenery_path; scenery)
378    {
379    model_dir = scenery_path ~ "/Models/Airport/Jetway";
380    model_file = model_dir ~ "/" ~ model ~ ".xml";
381    airline_file = model_dir ~ "/" ~ model ~ ".airline." ~ airline ~ ".xml";
382    print_debug("Trying to load a jetway model from " ~ model_file);
383    if (io.stat(model_file) == nil) continue;
384    model_tree = io.read_properties(model_file);
385    if (io.stat(airline_file) != nil) props.copy(io.read_properties(airline_file), model_tree);
386    break;
387    }
388   if (model_tree == nil)
389    {
390    model_dir = root ~ "/Models/Airport/Jetway";
391    model_file = model_dir ~ "/" ~ model ~ ".xml";
392    airline_file = model_dir ~ "/" ~ model ~ ".airline." ~ airline ~ ".xml";
393    print_debug("Falling back to " ~ model_file);
394    if (io.stat(model_file) == nil)
395     {
396     print_error("Failed to load jetway model: " ~ model);
397     return;
398     }
399    model_tree = io.read_properties(model_file);
400    if (io.stat(airline_file) != nil) props.copy(io.read_properties(airline_file), model_tree);
401    }
402
403   var m =
404    {
405    parents: [Jetway]
406    };
407   m._active = 1; # set this to 'true' on the first run so that the offsets can take effect
408   m._edit = 0;
409   m.airport = airport;
410   m.gate = gate;
411   m.airline = airline;
412   m.id = id;
413   m.model = model;
414   m.extended = 0;
415   m.door = door;
416   m.lat = lat;
417   m.lon = lon;
418   m.alt = alt;
419   m.heading = geo.normdeg(180 - heading);
420   m.init_extend = init_extend;
421   m.init_heading = init_heading;
422   m.init_pitch = init_pitch;
423   m.init_ent_heading = init_ent_heading;
424   m.target_extend = 0;
425   m.target_pitch = 0;
426   m.target_heading = 0;
427   m.target_ent_heading = 0;
428   m.target_hood = 0;
429   m.rotunda_x = model_tree.getNode("rotunda/x-m").getValue();
430   m.rotunda_y = model_tree.getNode("rotunda/y-m").getValue();
431   m.rotunda_z = model_tree.getNode("rotunda/z-m").getValue();
432   m.offset_extend = model_tree.getNode("extend-offset-m").getValue();
433   m.offset_entrance = model_tree.getNode("entrance-offset-m").getValue();
434   m.min_extend = model_tree.getNode("min-extend-m").getValue();
435   m.max_extend = model_tree.getNode("max-extend-m").getValue();
436
437   # get the runtime file path
438   if (runtime_file == runtime_files)
439    {
440    runtime_file = 0;
441    }
442   var runtime_file_path = home ~ "/runtime-jetways/" ~ runtime_file ~ ".xml";
443   runtime_file += 1;
444
445   # create the model node and the door object
446   m.node = putmodel(runtime_file_path, lat, lon, alt, geo.normdeg(360 - heading));
447   var node_path = m.node.getPath();
448   m.door_object = aircraft.door.new(node_path ~ "/jetway-position", 0);
449
450   # manipulate the model tree
451   model_tree.getNode("path").setValue(model_dir ~ "/" ~ model_tree.getNode("path").getValue());
452   model_tree.getNode("toggle-action-script").setValue("jetways.toggle_jetway(" ~ id ~ ");");
453   model_tree.getNode("gate").setValue(m.gate);
454   model_tree.getNode("extend-m").setValue(props.globals.initNode(node_path ~ "/jetway-position/extend-m", 0, "DOUBLE").getPath());
455   model_tree.getNode("pitch-deg").setValue(props.globals.initNode(node_path ~ "/jetway-position/pitch-deg", 0, "DOUBLE").getPath());
456   model_tree.getNode("heading-deg").setValue(props.globals.initNode(node_path ~ "/jetway-position/heading-deg", 0, "DOUBLE").getPath());
457   model_tree.getNode("entrance-heading-deg").setValue(props.globals.initNode(node_path ~ "/jetway-position/entrance-heading-deg", 0, "DOUBLE").getPath());
458   model_tree.getNode("hood-deg").setValue(props.globals.initNode(node_path ~ "/jetway-position/hood-deg", 0, "DOUBLE").getPath());
459   # airline texture
460   var airline_tex = model_tree.getNode("airline-texture-path", 1).getValue();
461   var airline_node = model_tree.getNode(model_tree.getNode("airline-prop-path", 1).getValue());
462   if (airline_tex != nil and airline_node != nil)
463    {
464    airline_node.setValue(get_relative_filepath(home ~ "/runtime-jetways", model_dir ~ "/" ~ airline_tex));
465    }
466   # write the model tree
467   io.write_properties(runtime_file_path, model_tree);
468
469   jetways[id] = m;
470   print_debug("Loaded jetway #" ~ id);
471   jetway_id_prop.setValue(id);
472   return m;
473   },
474  toggle: func(user, heading, coord, hood = 0)
475   {
476   me._active = 1;
477   if (me.extended)
478    {
479    me.retract(user, heading, coord);
480    }
481   else
482    {
483    me.extend(user, heading, coord, hood);
484    }
485   },
486  extend: func(user, heading, door_coord, hood = 0)
487   {
488   me.extended = 1;
489
490   # get the coordinates of the jetway and offset for the rotunda position
491   var jetway_coord = geo.Coord.new();
492   jetway_coord.set_latlon(me.lat, me.lon);
493   jetway_coord.apply_course_distance(me.heading, me.rotunda_x);
494   jetway_coord.apply_course_distance(me.heading - 90, me.rotunda_y);
495   jetway_coord.set_alt(me.alt + me.rotunda_z);
496
497   if (debug_switch.getBoolValue())
498    {
499    # place UFO cursors at the calculated door and jetway positions for debugging purposes
500    geo.put_model("Aircraft/ufo/Models/cursor.ac", door_coord);
501    geo.put_model("Aircraft/ufo/Models/cursor.ac", jetway_coord);
502    }
503
504   # offset the door for the length of the jetway entrance
505   door_coord.apply_course_distance(heading - 90, me.offset_entrance);
506
507   # calculate the bearing to the aircraft and the distance from the door
508   me.target_heading = normdeg(jetway_coord.course_to(door_coord) - me.heading - me.init_heading);
509   me.target_extend = jetway_coord.distance_to(door_coord) - me.offset_extend - me.init_extend;
510
511   # check if distance exceeds maximum jetway extension length
512   if (me.target_extend + me.init_extend > me.max_extend)
513    {
514    me.extended = 0;
515    me.target_extend = 0;
516    me.target_heading = 0;
517    if (user) alert("Your aircraft is too far from this jetway.");
518    print_debug("Jetway #" ~ me.id ~ " is too far from the door");
519    return;
520    }
521   # check if distance fails to meet minimum jetway extension length
522   if (me.target_extend + me.init_extend < me.min_extend)
523    {
524    me.extended = 0;
525    me.target_extend = 0;
526    me.target_heading = 0;
527    if (user) alert("Your aircraft is too close to this jetway.");
528    print_debug("Jetway #" ~ me.id ~ " is too close to the door");
529    return;
530    }
531
532   # calculate the jetway pitch, entrance heading, and hood
533   me.target_pitch = math.atan2((door_coord.alt() - jetway_coord.alt()) / (me.target_extend + me.offset_extend + me.init_extend), 1) * R2D - me.init_pitch;
534   me.target_ent_heading = normdeg((heading + 90) - (me.heading + (me.target_heading + me.init_heading) + me.init_ent_heading));
535   me.target_hood = user ? getprop("/sim/model/door[" ~ me.door ~ "]/jetway-hood-deg") : hood;
536
537   # fire up the animation
538   if (user) alert("Extending jetway.");
539   var animation_time = math.abs(me.target_extend / extend_rate) + math.abs(me.target_pitch / pitch_rate) + math.abs(me.target_heading / heading_rate) + math.abs(me.target_ent_heading / heading_entrance_rate) + math.abs(me.target_hood / hood_rate);
540   me.door_object.swingtime = animation_time;
541   me.door_object.open();
542
543   print_debug("************************************************");
544   print_debug("Activated jetway #" ~ me.id);
545   print_debug("Using door #" ~ me.door);
546   print_debug("Jetway heading:          " ~ me.heading ~ " deg");
547   print_debug("Extension:               " ~ me.target_extend ~ " m");
548   print_debug("Pitch:                   " ~ me.target_pitch ~ " deg");
549   print_debug("Heading:         " ~ me.target_heading ~ " deg");
550   print_debug("Entrance heading:        " ~ me.target_ent_heading ~ " deg");
551   print_debug("Hood:                    " ~ me.target_hood ~ " deg");
552   print_debug("Total animation time:    " ~ animation_time ~ " sec");
553   print_debug("Jetway extending");
554   print_debug("************************************************");
555   },
556  retract: func(user)
557   {
558   if (user) alert("Retracting jetway.");
559   me.door_object.close();
560   me.extended = 0;
561
562   print_debug("************************************************");
563   print_debug("Activated jetway #" ~ me.id);
564   print_debug("Total animation time:    " ~ me.door_object.swingtime ~ " sec");
565   print_debug("Jetway retracting");
566   print_debug("************************************************");
567   },
568  remove: func
569   {
570   me.node.remove();
571   var id = me.id;
572   jetways[me.id] = nil;
573   print_debug("Unloaded jetway #" ~ id);
574   },
575  reload: func
576   {
577   var airport = me.airport;
578   var model = me.model;
579   var gate = me.gate;
580   var door = me.door;
581   var airline = me.airline;
582   var lat = me.lat;
583   var lon = me.lon;
584   var alt = me.alt;
585   var heading = geo.normdeg(180 - (me.heading - 360));
586   var init_extend = me.init_extend;
587   var init_heading = me.init_heading;
588   var init_pitch = me.init_pitch;
589   var init_ent_heading = me.init_ent_heading;
590   me.remove();
591   Jetway.new(airport, model, gate, door, airline, lat, lon, alt, heading, init_extend, init_heading, init_pitch, init_ent_heading);
592   },
593  setpos: func(lat, lon, hdg, alt)
594   {
595   me.node.getNode("latitude-deg").setValue(lat);
596   me.lat = lat;
597   me.node.getNode("longitude-deg").setValue(lon);
598   me.lon = lon;
599   me.node.getNode("heading-deg").setValue(geo.normdeg(hdg - 180));
600   me.heading = hdg;
601   me.node.getNode("elevation-ft").setValue(alt * M2FT);
602   me.alt = alt;
603   },
604  setmodel: func(model, airline, gate)
605   {
606   me.airline = airline;
607   me.gate = gate;
608   me.model = model;
609   me.extended = 0;
610   me.target_extend = 0;
611   me.target_pitch = 0;
612   me.target_heading = 0;
613   me.target_ent_heading = 0;
614   me.target_hood = 0;
615   me.door_object.setpos(0);
616   me.reload();
617   }
618  };
619
620 ## Interaction functions
621 ########################
622
623 var dialog = func
624  {
625  if (dialog_object == nil) dialog_object = gui.Dialog.new("/sim/gui/dialogs/jetways/dialog", "gui/dialogs/jetways.xml");
626  dialog_object.open();
627  };
628
629 var toggle_jetway = func(n)
630  {
631  var jetway = jetways[n];
632  if (jetway == nil) return;
633  var door = props.globals.getNode("/sim/model/door[" ~ jetway.door ~ "]");
634  if (door == nil)
635   {
636   alert("Your aircraft does not define the location of door " ~ (jetway.door + 1) ~ ", cannot extend this jetway.");
637   return;
638   }
639
640  # get the coordinates of the user's aircraft and offset for the door position and aircraft pitch
641  var coord = geo.aircraft_position();
642  var heading = getprop("/orientation/heading-deg");
643  var pitch = getprop("/orientation/pitch-deg");
644  coord.apply_course_distance(heading, -door.getChild("position-x-m").getValue());
645  coord.apply_course_distance(heading + 90, door.getChild("position-y-m").getValue());
646  coord.set_alt(coord.alt() + door.getChild("position-z-m").getValue());
647  coord.set_alt(coord.alt() + math.tan(pitch * D2R) * -door.getChild("position-x-m").getValue());
648
649  jetway.toggle(1, heading, coord);
650  };
651 var toggle_jetway_from_coord = func(door, hood, heading, lat, lon = nil)
652  {
653  if (isa(lat, geo.Coord))
654   {
655   var coord = lat;
656   }
657  else
658   {
659   var coord = geo.Coord.new();
660   coord.set_latlon(lat, lon);
661   }
662  var closest_jetway = nil;
663  var closest_jetway_dist = nil;
664  var closest_jetway_coord = nil;
665  foreach (var jetway; jetways)
666   {
667   if (jetway == nil) continue;
668   var jetway_coord = geo.Coord.new();
669   jetway_coord.set_latlon(jetway.lat, jetway.lon);
670
671   var distance = jetway_coord.distance_to(coord);
672   if ((closest_jetway_dist == nil or distance < closest_jetway_dist) and jetway.door == door)
673    {
674    closest_jetway = jetway;
675    closest_jetway_dist = distance;
676    closest_jetway_coord = jetway_coord;
677    }
678   }
679  if (closest_jetway == nil)
680   {
681   print_debug("No jetways available");
682   }
683  elsif (!closest_jetway.extended)
684   {
685   closest_jetway.toggle(0, heading, coord, hood);
686   }
687  };
688 var toggle_jetway_from_model = func(model)
689  {
690  model = aircraft.makeNode(model);
691  var doors = model.getChildren("door");
692  if (doors == nil or size(doors) == 0) return;
693  for (var i = 0; i < size(doors); i += 1)
694   {
695   var coord = geo.Coord.new();
696   var hdg = model.getNode("orientation/true-heading-deg").getValue();
697   var lat = model.getNode("position/latitude-deg").getValue();
698   var lon = model.getNode("position/longitude-deg").getValue();
699   var alt = model.getNode("position/altitude-ft").getValue() * FT2M + doors[i].getNode("position-z-m").getValue();
700   coord.set_latlon(lat, lon, alt);
701   coord.apply_course_distance(hdg, -doors[i].getNode("position-x-m").getValue());
702   coord.apply_course_distance(hdg + 90, doors[i].getNode("position-y-m").getValue());
703   print_debug("Connecting a jetway to door #" ~ i ~ " for model " ~ model.getPath());
704   toggle_jetway_from_coord(i, doors[i].getNode("jetway-hood-deg").getValue(), hdg, coord);
705   }
706  };
707
708 ## Internal functions
709 #####################
710
711 # loads jetways at an airport
712 var load_airport_jetways = func(airport)
713  {
714  if (isin(loaded_airports, airport)) return;
715  var tree = io.read_airport_properties(airport, "jetways");
716  if (tree == nil)
717   {
718   tree = io.read_properties(root ~ "/AI/Airports/" ~ airport ~ "/jetways.xml");
719   if (tree == nil) return;
720   }
721  append(loaded_airports, airport);
722  print_debug("Loading jetways for airport " ~ airport);
723  var nodes = tree.getChildren("jetway");
724
725  loadids[airport] = loadids[airport] == nil ? 0 : loadids[airport] + 1;
726  var i = 0;
727  var loop = func(id)
728   {
729   if (id != loadids[airport]) return;
730   if (i >= size(nodes)) return;
731   var jetway = nodes[i];
732   var model = jetway.getNode("model", 1).getValue() or return;
733   var gate = jetway.getNode("gate", 1).getValue() or "";
734   var door = jetway.getNode("door", 1).getValue() or 0;
735   var airline = jetway.getNode("airline", 1).getValue() or "None";
736   var lat = jetway.getNode("latitude-deg", 1).getValue() or return;
737   var lon = jetway.getNode("longitude-deg", 1).getValue() or return;
738   var elev = jetway.getNode("elevation-m", 1).getValue() or 0;
739   var alt = geo.elevation(lat, lon) + elev;
740   var heading = jetway.getNode("heading-deg", 1).getValue() or 0;
741   var init_extend = jetway.getNode("initial-position/jetway-extension-m", 1).getValue() or 0;
742   var init_heading = jetway.getNode("initial-position/jetway-heading-deg", 1).getValue() or 0;
743   var init_pitch = jetway.getNode("initial-position/jetway-pitch-deg", 1).getValue() or 0;
744   var init_ent_heading = jetway.getNode("initial-position/entrance-heading-deg", 1).getValue() or 0;
745   Jetway.new(airport, model, gate, door, airline, lat, lon, alt, heading, init_extend, init_heading, init_pitch, init_ent_heading);
746
747   i += 1;
748   settimer(func loop(id), LOAD_JETWAY_PERIOD);
749   };
750  settimer(func loop(loadids[airport]), 0);
751  };
752 # unloads jetways at an airport
753 var unload_airport_jetways = func(airport)
754  {
755  print_debug("Unloading jetways for airport " ~ airport);
756  foreach (var jetway; jetways)
757   {
758   if (jetway != nil and jetway.airport == airport) jetway.remove();
759   }
760  remove(loaded_airports, airport);
761  };
762
763 # restarts the main update loop
764 var restart = func()
765  {
766  update_loopid += 1;
767  update_jetways(update_loopid);
768  settimer(func
769   {
770   load_loopid += 1;
771   load_jetways(load_loopid);
772   }, 2);
773  print("Animated jetways ... initialized");
774  };
775 # main update loop (runs when jetways are enable and actived)
776 var update_jetways = func(loopid)
777  {
778  # terminate if loopid does not match
779  if (loopid != update_loopid) return;
780  # if jetways disabled, unload jetways and terminate
781  if (!on_switch.getBoolValue())
782   {
783  foreach (var jetway; jetways)
784    {
785    if (jetway != nil) jetway.remove();
786    }
787   setsize(jetways, 0);
788   setsize(loaded_airports, 0);
789   return;
790   }
791  # interpolate jetway values
792  foreach (var jetway; jetways)
793   {
794   if (jetway == nil) continue;
795   if (jetway._active or jetway._edit)
796    {
797    var position = jetway.door_object.getpos();
798    if (position == 0 or position == 1) jetway._active = 0;
799    jetway.node.getNode("jetway-position/extend-m").setValue(interpolate_table(extend_table, position) * jetway.target_extend + jetway.init_extend);
800    jetway.node.getNode("jetway-position/pitch-deg").setValue(interpolate_table(pitch_table, position) * jetway.target_pitch + jetway.init_pitch);
801    jetway.node.getNode("jetway-position/heading-deg").setValue(interpolate_table(heading_table, position) * jetway.target_heading + jetway.init_heading);
802    jetway.node.getNode("jetway-position/entrance-heading-deg").setValue(interpolate_table(heading_entrance_table, position) * jetway.target_ent_heading + jetway.init_ent_heading);
803    jetway.node.getNode("jetway-position/hood-deg").setValue(interpolate_table(hood_table, position) * jetway.target_hood);
804    }
805   }
806  settimer(func update_jetways(loopid), UPDATE_PERIOD);
807  };
808 # loading/unloading loop (runs continuously)
809 var load_jetways = func(loopid)
810  {
811  if (load_listenerid != nil) removelistener(load_listenerid);
812  # terminate if loopid does not match
813  # unloading jetways if jetways are disabled is handled by update loop
814  if (loopid != load_loopid or !on_switch.getBoolValue()) return;
815  var airports = find_airports(LOAD_DISTANCE);
816  if (airports == nil) return;
817  # search for any airports out of range and unload their jetways
818  foreach (var airport; loaded_airports)
819   {
820   if (!isin(airports, airport))
821    {
822    unload_airport_jetways(airport);
823    }
824   }
825  # load any airports in range
826  foreach (var airport; airports)
827   {
828   load_airport_jetways(airport);
829   }
830
831  var nearest_airport = airportinfo();
832  nearest_airport = nearest_airport == nil ? nil : nearest_airport.id;
833  if (isin(loaded_airports, nearest_airport))
834   {
835   # loop through the AI aircraft and extend/retract jetways
836   var ai_aircraft = props.globals.getNode("ai/models").getChildren("aircraft");
837   foreach (var aircraft; ai_aircraft)
838    {
839    if (!aircraft.getNode("valid", 1).getBoolValue()) continue;
840    var connected = aircraft.getNode("connected-to-jetways", 1);
841    var velocity = aircraft.getNode("velocities/true-airspeed-kt", 1).getValue();
842    # TODO: Find a better way to know when the aircraft is "parked"
843    if (velocity != nil and velocity > -1 and velocity < 1)
844     {
845     if (!connected.getBoolValue()) toggle_jetway_from_model(aircraft);
846     connected.setBoolValue(1);
847     }
848    else
849     {
850     if (connected.getBoolValue()) toggle_jetway_from_model(aircraft);
851     connected.setBoolValue(0);
852     }
853    }
854   # loop through the multiplayer aircraft and extend/retract jetways
855   # TODO: In the future, broadcast jetway properties over MP, making this part obselete
856   if (mp_switch.getBoolValue())
857    {
858    var multiplayers = props.globals.getNode("ai/models").getChildren("multiplayer");
859    foreach (var aircraft; multiplayers)
860     {
861     if (!aircraft.getNode("valid", 1).getBoolValue()) continue;
862     var connected = aircraft.getNode("connected-to-jetways", 1);
863     var velocity = aircraft.getNode("velocities/true-airspeed-kt", 1).getValue();
864     if (velocity != nil and velocity > -1 and velocity < 1)
865      {
866      if (!connected.getBoolValue()) toggle_jetway_from_model(aircraft);
867      connected.setBoolValue(1);
868      }
869     else
870      {
871      if (connected.getBoolValue()) toggle_jetway_from_model(aircraft);
872      connected.setBoolValue(0);
873      }
874     }
875    }
876   }
877  settimer(func load_jetways(loopid), LOAD_PERIOD);
878  };
879 ## fire it up
880 _setlistener("/nasal/jetways/loaded", func
881  {
882  # global variables
883  root = string.normpath(getprop("/sim/fg-root"));
884  home = string.normpath(getprop("/sim/fg-home"));
885  foreach (var scenery_path; props.globals.getNode("/sim").getChildren("fg-scenery"))
886   {
887   append(scenery, string.normpath(scenery_path.getValue()));
888   }
889  if (size(scenery) == 0) append(scenery, root ~ "/Scenery");
890
891  # properties
892  on_switch = props.globals.getNode("/nasal/jetways/enabled", 1);
893  debug_switch = props.globals.getNode("/sim/jetways/debug", 1);
894  mp_switch = props.globals.getNode("/sim/jetways/interact-with-multiplay", 1);
895
896  jetway_id_prop = props.globals.getNode(jetway_id_prop, 1);
897  restart();
898  });