Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / view.nas
1 ##
2 ## view.nas
3 ##
4 ##  Nasal code for implementing view-specific functionality.
5
6 var index = nil;    # current view index
7 var views = nil;    # list of all view branches (/sim/view[n]) as props.Node
8 var current = nil;  # current view branch (e.g. /sim/view[1]) as props.Node
9 var fovProp = nil;
10
11 var hasmember = func(class, member) {
12         if (contains(class, member))
13                 return 1;
14         if (!contains(class, "parents"))
15                 return 0;
16         if (typeof(class.parents) != "vector")
17                 return 0;
18         foreach (var parent; class.parents)
19                 if (hasmember(parent, member))
20                         return 1;
21         return 0;
22 }
23
24
25 # Dynamically calculate limits so that it takes STEPS iterations to
26 # traverse the whole range, the maximum FOV is fixed at 120 degrees,
27 # and the minimum corresponds to normal maximum human visual acuity
28 # (~1 arc minute of resolution, although apparently people vary widely
29 # in this ability).  Quick derivation of the math:
30 #
31 #   mul^steps = max/min
32 #   steps * ln(mul) = ln(max/min)
33 #   mul = exp(ln(max/min) / steps)
34 var STEPS = 40;
35 var ACUITY = 1/60; # Maximum angle subtended by one pixel (== 1 arc minute)
36 var max = var min = var mul = 0;
37 var calcMul = func {
38     max = 120; # Fixed at 120 degrees
39     min = getprop("/sim/startup/xsize") * ACUITY;
40     mul = math.exp(math.ln(max/min) / STEPS);
41 }
42
43 ##
44 # Handler.  Increase FOV by one step
45 #
46 var increase = func {
47     calcMul();
48     var val = fovProp.getValue() * mul;
49     if(val == max) { return; }
50     if(val > max) { val = max }
51     fovProp.setDoubleValue(val);
52     var popup=getprop("/sim/view-name-popup");
53     if(popup == 1 or popup==nil) gui.popupTip(sprintf("FOV: %.1f", val));
54 }
55
56 ##
57 # Handler.  Decrease FOV by one step
58 #
59 var decrease = func {
60     calcMul();
61     var val = fovProp.getValue() / mul;
62     fovProp.setDoubleValue(val);
63     var popup=getprop("/sim/view-name-popup");
64     if(popup == 1 or popup==nil) gui.popupTip(sprintf("FOV: %.1f%s", val, val < min ? " (overzoom)" : ""));
65 }
66
67 ##
68 # Handler.  Reset FOV to default.
69 #
70 var resetFOV = func {
71     setprop("/sim/current-view/field-of-view",
72             getprop("/sim/current-view/config/default-field-of-view-deg"));
73 }
74
75 var resetViewPos = func {
76     var v = current.getNode("config");
77     setprop("/sim/current-view/x-offset-m", v.getNode("x-offset-m", 1).getValue() or 0);
78     setprop("/sim/current-view/y-offset-m", v.getNode("y-offset-m", 1).getValue() or 0);
79     setprop("/sim/current-view/z-offset-m", v.getNode("z-offset-m", 1).getValue() or 0);
80 }
81
82 var resetViewDir = func {
83     var v = current.getNode("config");
84     setprop("/sim/current-view/heading-offset-deg", v.getNode("heading-offset-deg", 1).getValue() or 0);
85     setprop("/sim/current-view/pitch-offset-deg", v.getNode("pitch-offset-deg", 1).getValue() or 0);
86     setprop("/sim/current-view/roll-offset-deg", v.getNode("roll-offset-deg", 1).getValue() or 0);
87 }
88
89 ##
90 # Handler.  Step to the next (force=1) or next enabled view.
91 #
92 var stepView = func(step, force = 0) {
93     step = step > 0 ? 1 : -1;
94     var n = index;
95     for (var i = 0; i < size(views); i += 1) {
96         n += step;
97         if (n < 0)
98             n = size(views) - 1;
99         elsif (n >= size(views))
100             n = 0;
101         var e = views[n].getNode("enabled");
102         if (force or (e == nil or e.getBoolValue()) and
103             (views[n].getNode("name")!=nil))
104             break;
105     }
106     setprop("/sim/current-view/view-number", n);
107
108     # And pop up a nice reminder
109     var popup=getprop("/sim/view-name-popup");
110     if(popup == 1 or popup==nil) gui.popupTip(views[n].getNode("name").getValue());
111 }
112
113 ##
114 # Get view index by name.
115 #
116 var indexof = func(name) {
117     forindex (var i; views)
118         if (views[i].getNode("name", 1).getValue() == name)
119             return i;
120     return nil;
121 }
122
123 ##
124 # Standard view "slew" rate, in degrees/sec.
125 #
126 var VIEW_PAN_RATE = 60;
127
128 ##
129 # Pans the view horizontally.  The argument specifies a relative rate
130 # (or number of "steps" -- same thing) to the standard rate.
131 #
132 var panViewDir = func(step) {
133     if (getprop("/sim/freeze/master"))
134         var prop = "/sim/current-view/heading-offset-deg";
135     else
136         var prop = "/sim/current-view/goal-heading-offset-deg";
137
138     controls.slewProp(prop, step * VIEW_PAN_RATE);
139 }
140
141 ##
142 # Pans the view vertically.  The argument specifies a relative rate
143 # (or number of "steps" -- same thing) to the standard rate.
144 #
145 var panViewPitch = func(step) {
146     if (getprop("/sim/freeze/master"))
147         var prop = "/sim/current-view/pitch-offset-deg";
148     else
149         var prop = "/sim/current-view/goal-pitch-offset-deg";
150
151     controls.slewProp(prop, step * VIEW_PAN_RATE);
152 }
153
154
155 ##
156 # Reset view to default using current view manager (see default_handler).
157 #
158 var resetView = func {
159         manager.reset();
160 }
161
162
163 ##
164 # Default view handler used by view.manager.
165 #
166 var default_handler = {
167         reset : func {
168                 resetViewDir();
169                 resetFOV();
170         },
171 };
172
173
174 ##
175 # View manager. Administrates optional Nasal view handlers.
176 # Usage:  view.manager.register(<view-id>, <view-handler>);
177 #
178 #   view-id:      the view's name (e.g. "Chase View") or index number
179 #   view-handler: a hash with any combination of the functions listed in the
180 #                 following example, or none at all. Only define the interface
181 #                 functions that you really need! The hash may contain local
182 #                 variables and other, non-interface functions.
183 #
184 # Example:
185 #
186 #   var some_view_handler = {
187 #           init   : func {},    # called only once at startup
188 #           start  : func {},    # called when view is switched to our view
189 #           stop   : func {},    # called when view is switched away from our view
190 #           reset  : func {},    # called with view.resetView()
191 #           update : func { 0 }, # called iteratively if defined. Must return
192 #   };                           # interval in seconds until next invocation
193 #                                # Don't define it if you don't need it!
194 #
195 #   view.manager.register("Some View", some_view_handler);
196 #
197 #
198 var manager = {
199         current : { node: nil, handler: default_handler },
200         init : func {
201                 me.views = {};
202                 me.loopid = 0;
203                 var viewnodes = props.globals.getNode("sim").getChildren("view");
204                 forindex (var i; viewnodes)
205                         me.views[i] = { node: viewnodes[i], handler: default_handler };
206                 setlistener("/sim/current-view/view-number", func(n) {
207                         manager.set_view(n.getValue());
208                 }, 1);
209         },
210         register : func(which, handler = nil) {
211                 if (num(which) == nil)
212                         which = indexof(which);
213                 if (handler == nil)
214                         handler = default_handler;
215                 me.views[which]["handler"] = handler;
216                 if (hasmember(handler, "init"))
217                         handler.init(me.views[which].node);
218                 me.set_view();
219         },
220         set_view : func(which = nil) {
221                 if (which == nil)
222                         which = index;
223                 elsif (num(which) == nil)
224                         which = indexof(which);
225
226                 me.loopid += 1;
227                 if (hasmember(me.current.handler, "stop"))
228                         me.current.handler.stop();
229
230                 me.current = me.views[which];
231
232                 if (hasmember(me.current.handler, "start"))
233                         me.current.handler.start();
234                 if (hasmember(me.current.handler, "update"))
235                         me._loop_(me.loopid += 1);
236                 screenWidthCompens.update();
237         },
238         reset : func {
239                 if (hasmember(me.current.handler, "reset"))
240                         me.current.handler.reset();
241                 else
242                         default_handler.reset();
243         },
244         _loop_ : func(id) {
245                 id == me.loopid or return;
246                 settimer(func { me._loop_(id) }, me.current.handler.update() or 0);
247         },
248 };
249
250
251 var fly_by_view_handler = {
252         init : func {
253                 me.latN = props.globals.getNode("/sim/viewer/latitude-deg", 1);
254                 me.lonN = props.globals.getNode("/sim/viewer/longitude-deg", 1);
255                 me.altN = props.globals.getNode("/sim/viewer/altitude-ft", 1);
256                 me.vnN = props.globals.getNode("/velocities/speed-north-fps", 1);
257                 me.veN = props.globals.getNode("/velocities/speed-east-fps", 1);
258                 me.hdgN = props.globals.getNode("/orientation/heading-deg", 1);
259
260                 setlistener("/sim/signals/reinit", func(n) { n.getValue() or me.reset() });
261                 setlistener("/sim/crashed", func(n) { n.getValue() and me.reset() });
262                 setlistener("/sim/freeze/replay-state", func {
263                         settimer(func { me.reset() }, 1); # time for replay to catch up
264                 });
265                 me.reset();
266         },
267         start : func {
268                 me.reset();
269         },
270         reset: func {
271                 me.chase = -getprop("/sim/chase-distance-m");
272                 # me.course = me.hdgN.getValue();
273                 var vn = me.vnN.getValue();
274                 var ve = me.veN.getValue();
275                 me.course = (0.5*math.pi - math.atan2(vn, ve))*R2D;
276                 
277                 me.last = geo.aircraft_position();
278                 me.setpos(1);
279                 # me.dist = 20;
280         },
281         setpos : func(force = 0) {
282                 var pos = geo.aircraft_position();
283                 var vn = me.vnN.getValue();
284                 var ve = me.veN.getValue();
285
286                 var dist = 0.0;
287                 if ( force ) {
288                     # predict distance based on speed
289                     var mps = math.sqrt( vn*vn + ve*ve ) * FT2M;
290                     dist = mps * 3.5; # 3.5 seconds worth of travel
291                 } else {
292                     # use actual distance
293                     dist = me.last.distance_to(pos);
294                     # reset when too far (i.e. position changed due to skipping time in replay mode)
295                     if (dist>5000) return me.reset();
296                 }
297
298                 # check if the aircraft has moved enough
299                 if (dist < 1.7 * me.chase and !force)
300                         return 1.13;
301
302                 # "predict" and remember next aircraft position
303                 # var course = me.hdgN.getValue();
304                 var course = (0.5*math.pi - math.atan2(vn, ve))*R2D;
305                 var delta_alt = (pos.alt() - me.last.alt()) * 0.5;
306                 pos.apply_course_distance(course, dist * 0.8);
307                 pos.set_alt(pos.alt() + delta_alt);
308                 me.last.set(pos);
309
310                 # apply random deviation
311                 var radius = me.chase * (0.5 * rand() + 0.7);
312                 var agl = getprop("/position/altitude-agl-ft") * FT2M;
313                 if (agl > me.chase)
314                         var angle = rand() * 2 * math.pi;
315                 else
316                         var angle = ((2 * rand() - 1) * 0.15 + 0.5) * (rand() < 0.5 ? -math.pi : math.pi);
317
318                 var dev_alt = math.cos(angle) * radius;
319                 var dev_side = math.sin(angle) * radius;
320                 pos.apply_course_distance(course + 90, dev_side);
321
322                 # and make sure it's not under ground
323                 var lat = pos.lat();
324                 var lon = pos.lon();
325                 var alt = pos.alt();
326                 var elev = geo.elevation(lat, lon);
327                 if (elev != nil) {
328                         elev += 2;   # min elevation
329                         if (alt + dev_alt < elev and dev_alt < 0)
330                                 dev_alt = -dev_alt;
331                         if (alt + dev_alt < elev)
332                                 alt = elev;
333                         else
334                                 alt += dev_alt;
335                 }
336
337                 # set new view point
338                 me.latN.setValue(lat);
339                 me.lonN.setValue(lon);
340                 me.altN.setValue(alt * M2FT);
341                 return 7.3;
342         },
343         update : func {
344                 return me.setpos();
345         },
346 };
347
348
349 var model_view_handler = {
350         init: func(node) {
351                 me.viewN = node;
352                 me.current = nil;
353                 me.legendN = props.globals.initNode("/sim/current-view/model-view", "");
354                 me.dialog = props.Node.new({ "dialog-name": "model-view" });
355                 me.listener = nil;
356         },
357         start: func {
358                 me.listener = setlistener("/sim/signals/multiplayer-updated", func me._update_(), 1);
359                 me.reset();
360                 fgcommand("dialog-show", me.dialog);
361         },
362         stop: func {
363                 fgcommand("dialog-close", me.dialog);
364                 if (me.listener!=nil)
365                 {
366                         removelistener(me.listener);
367                         me.listener=nil;
368                 }
369         },
370         reset: func {
371                 me.select(0);
372         },
373         find: func(callsign) {
374                 forindex (var i; me.list)
375                         if (me.list[i].callsign == callsign)
376                                 return i;
377                 return nil;
378         },
379         select: func(which, by_callsign=0) {
380                 if (by_callsign or num(which) == nil)
381                         which = me.find(which) or 0;  # turn callsign into index
382
383                 me.setup(me.list[which]);
384         },
385         next: func(step) {
386                 var i = me.find(me.current);
387                 i = i == nil ? 0 : math.mod(i + step, size(me.list));
388                 me.setup(me.list[i]);
389         },
390         _update_: func {
391                 var self = { callsign: getprop("/sim/multiplay/callsign"), model:,
392                                 node: props.globals, root: '/' };
393                 me.list = [self] ~ multiplayer.model.list;
394                 if (!me.find(me.current))
395                         me.select(0);
396         },
397         setup: func(data) {
398                 if (data.root == '/') {
399                         var zoffset = getprop("/sim/chase-distance-m");
400                         var ident = '[' ~ data.callsign ~ ']';
401                 } else {
402                         var zoffset = 70;
403                         var ident = '"' ~ data.callsign ~ '" (' ~ data.model ~ ')';
404                 }
405
406                 me.current = data.callsign;
407                 me.legendN.setValue(ident);
408                 setprop("/sim/current-view/z-offset-m", zoffset);
409
410                 me.viewN.getNode("config").setValues({
411                         "eye-lat-deg-path": data.root ~ "/position/latitude-deg",
412                         "eye-lon-deg-path": data.root ~ "/position/longitude-deg",
413                         "eye-alt-ft-path": data.root ~ "/position/altitude-ft",
414                         "eye-heading-deg-path": data.root ~ "/orientation/heading-deg",
415                         "target-lat-deg-path": data.root ~ "/position/latitude-deg",
416                         "target-lon-deg-path": data.root ~ "/position/longitude-deg",
417                         "target-alt-ft-path": data.root ~ "/position/altitude-ft",
418                         "target-heading-deg-path": data.root ~ "/orientation/heading-deg",
419                         "target-pitch-deg-path": data.root ~ "/orientation/pitch-deg",
420                         "target-roll-deg-path": data.root ~ "/orientation/roll-deg",
421                 });
422         },
423 };
424
425
426 var pilot_view_limiter = {
427         new : func {
428                 return { parents: [pilot_view_limiter] };
429         },
430         init : func {
431                 me.hdgN = props.globals.getNode("/sim/current-view/heading-offset-deg");
432                 me.xoffsetN = props.globals.getNode("/sim/current-view/x-offset-m");
433                 me.xoffset_lowpass = aircraft.lowpass.new(0.1);
434                 me.last_offset = 0;
435                 me.needs_start = 0;
436         },
437         start : func {
438                 var limits = current.getNode("config/limits", 1);
439                 me.left = {
440                         heading_max : abs(limits.getNode("left/heading-max-deg", 1).getValue() or 1000),
441                         threshold : abs(limits.getNode("left/x-offset-threshold-deg", 1).getValue() or 0),
442                         xoffset_max : abs(limits.getNode("left/x-offset-max-m", 1).getValue() or 0),
443                 };
444                 me.right = {
445                         heading_max : -abs(limits.getNode("right/heading-max-deg", 1).getValue() or 1000),
446                         threshold : -abs(limits.getNode("right/x-offset-threshold-deg", 1).getValue() or 0),
447                         xoffset_max : -abs(limits.getNode("right/x-offset-max-m", 1).getValue() or 0),
448                 };
449                 me.left.scale = me.left.xoffset_max / (me.left.heading_max - me.left.threshold);
450                 me.right.scale = me.right.xoffset_max / (me.right.heading_max - me.right.threshold);
451                 me.last_hdg = normdeg(me.hdgN.getValue());
452                 me.enable_xoffset = me.right.xoffset_max > 0.001 or me.left.xoffset_max > 0.001;
453
454                 me.needs_start = 0;
455         },
456         update : func {
457                 if (getprop("/devices/status/keyboard/ctrl"))
458                         return;
459
460     if( getprop("/sim/signals/reinit") )
461     {
462       me.needs_start = 1;
463       return;
464     }
465     else if( me.needs_start )
466       me.start();
467
468                 var hdg = normdeg(me.hdgN.getValue());
469                 if (abs(me.last_hdg - hdg) > 180)  # avoid wrap-around skips
470                         me.hdgN.setDoubleValue(hdg = me.last_hdg);
471                 elsif (hdg > me.left.heading_max)
472                         me.hdgN.setDoubleValue(hdg = me.left.heading_max);
473                 elsif (hdg < me.right.heading_max)
474                         me.hdgN.setDoubleValue(hdg = me.right.heading_max);
475                 me.last_hdg = hdg;
476
477                 # translate view on X axis to look far right or far left
478                 if (me.enable_xoffset) {
479                         var offset = 0;
480                         if (hdg > me.left.threshold)
481                                 offset = (me.left.threshold - hdg) * me.left.scale;
482                         elsif (hdg < me.right.threshold)
483                                 offset = (me.right.threshold - hdg) * me.right.scale;
484
485                         var new_offset = me.xoffset_lowpass.filter(offset);
486                         me.xoffsetN.setDoubleValue(me.xoffsetN.getValue() - me.last_offset + new_offset);
487                         me.last_offset = new_offset;
488                 }
489                 return 0;
490         },
491 };
492
493
494 var panViewDir = func(step) {   # FIXME overrides panViewDir function from above; needs better integration
495         if (getprop("/sim/freeze/master"))
496                 var prop = "/sim/current-view/heading-offset-deg";
497         else
498                 var prop = "/sim/current-view/goal-heading-offset-deg";
499         var viewVal = getprop(prop);
500         var delta = step * VIEW_PAN_RATE * getprop("/sim/time/delta-realtime-sec");
501         var viewValSlew = normdeg(viewVal + delta);
502         var headingMax = abs(current.getNode("config/limits/left/heading-max-deg", 1).getValue() or 1000);
503         var headingMin = -abs(current.getNode("config/limits/right/heading-max-deg", 1).getValue() or 1000);
504         if (viewValSlew > headingMax)
505                 viewValSlew = headingMax;
506         elsif (viewValSlew < headingMin)
507                 viewValSlew = headingMin;
508         setprop(prop, viewValSlew);
509 }
510
511
512 #------------------------------------------------------------------------------
513 #
514 # Saves/restores/moves the view point (position, orientation, field-of-view).
515 # Moves are interpolated with sinusoidal characteristic. There's only one
516 # instance of this class, available as "view.point".
517 #
518 # Usage:
519 #    view.point.save();        ... save current view and return reference to
520 #                                  saved values in the form of a props.Node
521 #
522 #    view.point.restore();     ... restore saved view parameters
523 #
524 #    view.point.move(<prop> [, <time>]);
525 #                              ... set view parameters from a props.Node with
526 #                                  optional move time in seconds. <prop> may be
527 #                                  nil, in which case nothing happens.
528 #
529 # A parameter set as expected by set() and returned by save() is a props.Node
530 # object containing any (or none) of these children:
531 #
532 #   <heading-offset-deg>
533 #   <pitch-offset-deg>
534 #   <roll-offset-deg>
535 #   <x-offset-m>
536 #   <y-offset-m>
537 #   <z-offset-m>
538 #   <field-of-view>
539 #   <move-time-sec>
540 #
541 # The <move-time> isn't really a property of the view, but is available
542 # for convenience. The time argument in the move() method overrides it.
543
544
545 ##
546 # Normalize angle to  -180 <= angle < 180
547 #
548 var normdeg = func(a) {
549         while (a >= 180)
550                 a -= 360;
551         while (a < -180)
552                 a += 360;
553         return a;
554 }
555
556
557 ##
558 # Manages one translation/rotation axis. (For simplicity reasons the
559 # field-of-view parameter is also managed by this class.)
560 #
561 var ViewAxis = {
562         new : func(prop) {
563                 var m = { parents : [ViewAxis] };
564                 m.prop = props.globals.getNode(prop, 1);
565                 if (m.prop.getType() == "NONE")
566                         m.prop.setDoubleValue(0);
567
568                 m.from = m.to = m.prop.getValue();
569                 return m;
570         },
571         reset : func {
572                 me.from = me.to = normdeg(me.prop.getValue());
573         },
574         target : func(v) {
575                 me.to = normdeg(v);
576         },
577         move : func(blend) {
578                 me.prop.setValue(me.from + blend * (me.to - me.from));
579         },
580 };
581
582
583
584 ##
585 # view.point: handles smooth view movements
586 #
587 var point = {
588         init : func {
589                 me.axes = {
590                         "heading-offset-deg" : ViewAxis.new("/sim/current-view/goal-heading-offset-deg"),
591                         "pitch-offset-deg" : ViewAxis.new("/sim/current-view/goal-pitch-offset-deg"),
592                         "roll-offset-deg" : ViewAxis.new("/sim/current-view/goal-roll-offset-deg"),
593                         "x-offset-m" : ViewAxis.new("/sim/current-view/x-offset-m"),
594                         "y-offset-m" : ViewAxis.new("/sim/current-view/y-offset-m"),
595                         "z-offset-m" : ViewAxis.new("/sim/current-view/z-offset-m"),
596                         "field-of-view" : ViewAxis.new("/sim/current-view/field-of-view"),
597                 };
598                 me.storeN = props.Node.new();
599                 me.dtN = props.globals.getNode("/sim/time/delta-realtime-sec", 1);
600                 me.currviewN = props.globals.getNode("/sim/current-view", 1);
601                 me.blend = 0;
602                 me.loop_id = 0;
603                 props.copy(props.globals.getNode("/sim/view/config"), me.storeN);
604         },
605         save : func {
606                 me.storeN = props.Node.new();
607                 props.copy(me.currviewN, me.storeN);
608                 return me.storeN;
609         },
610         restore : func {
611                 me.move(me.storeN);
612         },
613         move : func(prop, time = nil) {
614                 prop != nil or return;
615                 var n = prop.getNode("view-number");
616                 if (n != nil)
617                         setprop("/sim/current-view/view-number",n.getValue());
618                 foreach (var a; keys(me.axes)) {
619                         var n = prop.getNode(a);
620                         me.axes[a].reset();
621                         if (n != nil)
622                                 me.axes[a].target(n.getValue());
623                 }
624                 var m = prop.getNode("move-time-sec");
625                 if (m != nil)
626                         time = m.getValue();
627
628                 if (time == nil)
629                         time = 1;
630
631                 me.blend = -1;   # range -1 .. 1
632                 me._loop_(me.loop_id += 1, time);
633         },
634         _loop_ : func(id, time) {
635                 me.loop_id == id or return;
636                 me.blend += me.dtN.getValue() / time;
637                 if (me.blend > 1)
638                         me.blend = 1;
639
640                 var b = (math.sin(me.blend * math.pi / 2) + 1) / 2; # range 0 .. 1
641                 foreach (var a; keys(me.axes))
642                         me.axes[a].move(b);
643
644                 if (me.blend < 1)
645                         settimer(func { me._loop_(id, time) }, 0);
646         },
647 };
648
649
650
651 ##
652 # view.ScreenWidthCompens: optional FOV compensation for wider screens.
653 # It keeps an equivalent of 55° FOV on a 4:3 zone centered on the screen
654 # whichever is the screen width/height ratio. Works only if width >= height.
655
656 var screenWidthCompens = {
657         defaultFov: nil,
658         oldW: nil, oldH: nil, oldOpt: nil,
659         assumedW: 4, assumedH: 3,
660         fovStore: [],
661         lastViewStatus: {},
662         statusNode: nil, # = /sim/current-view/field-of-view-compensation
663         getStatus: func me.statusNode.getValue(),
664         setStatus: func(state) me.statusNode.setValue(state),
665         wNode: nil, # = /sim/startup/xsize
666         hNode: nil, # = /sim/startup/ysize
667         getDimensions: func [me.wNode.getValue(),me.hNode.getValue()],
668         calcNewFov: func(fov=55, oldW=nil, oldH=nil, w=nil, h=nil) {
669                 var dim = me.getDimensions();
670                 if (w == nil) w = dim[0];
671                 if (h == nil) h = dim[1];
672                 if (oldW == nil) oldW = me.assumedW;
673                 if (oldH == nil) oldH = me.assumedH;
674                 if (w/h == oldW/oldH or h > w) return fov;
675                 else return math.atan2(w/h, oldW/oldH / math.tan(fov * D2R)) * R2D;
676         },
677         init: func() {
678                 me.defaultFov = getprop("/sim/current-view/config/default-field-of-view-deg");
679                 me.statusNode = props.globals.getNode("/sim/current-view/field-of-view-compensation", 1);
680                 me.wNode = props.globals.getNode("/sim/startup/xsize", 1);
681                 me.hNode = props.globals.getNode("/sim/startup/ysize", 1);
682                 (me.oldW, me.oldH) = me.getDimensions();
683
684                 setsize(me.fovStore, size(views));
685                 forindex (var i; views) {
686                         me.fovStore[i] = views[i].getNode("config/default-field-of-view-deg", 1).getValue() or 55;
687                         me.lastViewStatus[i] = { w:me.assumedW, h:me.assumedH };
688                 }
689                 me.update(opt:nil, force:1);
690         },
691         toggle: func() me.update(!me.getStatus(), 1),
692         update: func(opt=nil, force=0) {
693                 if (opt == nil)
694                         opt = me.getStatus();
695                 else me.setStatus(opt);
696                 var (w, h) = me.getDimensions();
697                 # Update config/default-field-of-view-deg nodes if state changed:
698                 if (force or me.oldOpt != opt or me.oldW/me.oldH != w/h) {
699                         me.oldW = w;
700                         me.oldH = h;
701                         me.oldOpt = opt;
702                         if (!opt) {
703                                 setprop("/sim/current-view/config/default-field-of-view-deg", me.defaultFov);
704                                 forindex (var i; views)
705                                         views[i].setValue("config/default-field-of-view-deg", me.fovStore[i]);
706                         } else {
707                                 setprop("/sim/current-view/config/default-field-of-view-deg",
708                                         me.calcNewFov(fov:me.defaultFov, w:w, h:h));
709                                 forindex (var i; views)
710                                         views[i].setValue("config/default-field-of-view-deg",
711                                                           me.calcNewFov(fov:me.fovStore[i], w:w, h:h));
712                         }
713                 }
714                 # Update this view if necessary:
715                 if (!opt) (w,h) = (me.assumedW,me.assumedH); # back to default FOV
716                 var thisview = me.lastViewStatus[index];
717                 if (thisview.w/thisview.h != w/h) {
718                         fovProp.setValue(me.calcNewFov(fovProp.getValue(), thisview.w, thisview.h, w, h))
719                         and
720                         ((thisview.opt,thisview.w,thisview.h) = (opt,w,h));
721                 }
722         },
723 };
724
725
726 _setlistener("/sim/signals/nasal-dir-initialized", func {
727         views = props.globals.getNode("/sim").getChildren("view");
728         fovProp = props.globals.getNode("/sim/current-view/field-of-view");
729         point.init();
730
731         setlistener("/sim/current-view/view-number", func(n) {
732                 current = views[index = n.getValue()];
733         }, 1);
734
735         props.globals.initNode("/position/altitude-agl-ft"); # needed by Fly-By View
736         screenWidthCompens.init();
737         manager.init();
738         manager.register("Fly-By View", fly_by_view_handler);
739         manager.register("Model View", model_view_handler);
740 });
741 _setlistener("/sim/signals/reinit", func {
742         screenWidthCompens.update(opt:nil,force:1);
743 });
744 _setlistener("/sim/startup/xsize", func {
745         screenWidthCompens.update();
746 });
747 _setlistener("/sim/startup/ysize", func {
748         screenWidthCompens.update();
749 });
750
751
752 var fdm_init_listener = _setlistener("/sim/signals/fdm-initialized", func {
753         removelistener(fdm_init_listener); # uninstall, so we're only called once
754         var zoffset = nil;
755         foreach (var v; views) {
756                 var index = v.getIndex();
757                 if (index > 7 and index < 100) {
758                         globals["view"] = nil;
759                         die("\n***\n*\n*  Illegal use of reserved view index "
760                                         ~ index ~ ". Use indices >= 100!\n*\n***");
761                 } elsif (index >= 100 and index < 200) {
762                         if (v.getNode("name") == nil)
763                                 continue;
764                         var e = v.getNode("enabled");
765                         if (e != nil) {
766                                 aircraft.data.add(e);
767                                 e.setAttribute("userarchive", 0);
768                         }
769                 }
770         }
771
772         forindex (var i; views) {
773                 var limits = views[i].getNode("config/limits/enabled");
774                 if (limits != nil) {
775                         func (i) {
776                                 var limiter = pilot_view_limiter.new();
777                                 setlistener(limits, func(n) {
778                                         manager.register(i, n.getBoolValue() ? limiter : nil);
779                                         manager.set_view();
780                                 }, 1);
781                         }(i);
782                 }
783         }
784 });