don't save view-enabled state of nameless views to aircraft config file
[fg:toms-fgdata.git] / Nasal / view.nas
1 ##
2 ## view.nas
3 ##
4 ##  Nasal code for implementing view-specific functionality.
5
6
7
8 # Dynamically calculate limits so that it takes STEPS iterations to
9 # traverse the whole range, the maximum FOV is fixed at 120 degrees,
10 # and the minimum corresponds to normal maximum human visual acuity
11 # (~1 arc minute of resolution, although apparently people vary widely
12 # in this ability).  Quick derivation of the math:
13 #
14 #   mul^steps = max/min
15 #   steps * ln(mul) = ln(max/min)
16 #   mul = exp(ln(max/min) / steps)
17 var STEPS = 40;
18 var ACUITY = 1/60; # Maximum angle subtended by one pixel (== 1 arc minute)
19 var max = var min = var mul = 0;
20 var calcMul = func {
21     max = 120; # Fixed at 120 degrees
22     min = getprop("/sim/startup/xsize") * ACUITY;
23     mul = math.exp(math.ln(max/min) / STEPS);
24 }
25
26 ##
27 # Handler.  Increase FOV by one step
28 #
29 var increase = func {
30     calcMul();
31     var val = fovProp.getValue() * mul;
32     if(val == max) { return; }
33     if(val > max) { val = max }
34     fovProp.setDoubleValue(val);
35     gui.popupTip(sprintf("FOV: %.1f", val));
36 }
37
38 ##
39 # Handler.  Decrease FOV by one step
40 #
41 var decrease = func {
42     calcMul();
43     var val = fovProp.getValue() / mul;
44     fovProp.setDoubleValue(val);
45     gui.popupTip(sprintf("FOV: %.1f%s", val, val < min ? " (overzoom)" : ""));
46 }
47
48 ##
49 # Handler.  Reset FOV to default.
50 #
51 var resetFOV = func {
52     setprop("/sim/current-view/field-of-view",
53             getprop("/sim/current-view/config/default-field-of-view-deg"));
54 }
55
56 var resetViewPos = func {
57     var v = views[getprop("/sim/current-view/view-number")].getNode("config");
58     setprop("/sim/current-view/x-offset-m", v.getNode("x-offset-m", 1).getValue() or 0);
59     setprop("/sim/current-view/y-offset-m", v.getNode("y-offset-m", 1).getValue() or 0);
60     setprop("/sim/current-view/z-offset-m", v.getNode("z-offset-m", 1).getValue() or 0);
61 }
62
63 var resetViewDir = func {
64     setprop("/sim/current-view/goal-heading-offset-deg",
65             getprop("/sim/current-view/config/heading-offset-deg"));
66     setprop("/sim/current-view/goal-pitch-offset-deg",
67             getprop("/sim/current-view/config/pitch-offset-deg"));
68     setprop("/sim/current-view/goal-roll-offset-deg",
69             getprop("/sim/current-view/config/roll-offset-deg"));
70 }
71
72 ##
73 # Handler.  Step to the next (force=1) or next enabled view.
74 #
75 var stepView = func(step, force = 0) {
76     step = step > 0 ? 1 : -1;
77     var n = getprop("/sim/current-view/view-number");
78     for (var i = 0; i < size(views); i += 1) {
79         n += step;
80         if (n < 0)
81             n = size(views) - 1;
82         elsif (n >= size(views))
83             n = 0;
84         if (force or (var e = views[n].getNode("enabled")) == nil or e.getBoolValue())
85             break;
86     }
87     setprop("/sim/current-view/view-number", n);
88
89     # And pop up a nice reminder
90     gui.popupTip(views[n].getNode("name").getValue());
91 }
92
93 ##
94 # Get view index by name.
95 #
96 var indexof = func(name) {
97     forindex (var i; views)
98         if (views[i].getNode("name", 1).getValue() == name)
99             return i;
100     return nil;
101 }
102
103 ##
104 # Standard view "slew" rate, in degrees/sec.
105 #
106 var VIEW_PAN_RATE = 60;
107
108 ##
109 # Pans the view horizontally.  The argument specifies a relative rate
110 # (or number of "steps" -- same thing) to the standard rate.
111 #
112 var panViewDir = func(step) {
113     if (getprop("/sim/freeze/master"))
114         var prop = "/sim/current-view/heading-offset-deg";
115     else
116         var prop = "/sim/current-view/goal-heading-offset-deg";
117
118     controls.slewProp(prop, step * VIEW_PAN_RATE);
119 }
120
121 ##
122 # Pans the view vertically.  The argument specifies a relative rate
123 # (or number of "steps" -- same thing) to the standard rate.
124 #
125 var panViewPitch = func(step) {
126     if (getprop("/sim/freeze/master"))
127         var prop = "/sim/current-view/pitch-offset-deg";
128     else
129         var prop = "/sim/current-view/goal-pitch-offset-deg";
130
131     controls.slewProp(prop, step * VIEW_PAN_RATE);
132 }
133
134
135 ##
136 # Reset view to default using current view manager (see default_handler).
137 #
138 var resetView = func {
139         manager.reset();
140 }
141
142
143 ##
144 # Default view handler used by view.manager.
145 #
146 var default_handler = {
147         reset : func {
148                 resetViewDir();
149                 resetFOV();
150         },
151 };
152
153
154 ##
155 # View manager. Administrates optional Nasal view handlers.
156 # Usage:  view.manager.register(<view-id>, <view-handler>);
157 #
158 #   view-id:      the view's name, e.g. "Chase View"
159 #   view-handler: a hash with any combination of the functions listed in the
160 #                 following example, or none at all. Only define the interface
161 #                 functions that you really need! The hash may contain local
162 #                 variables and other, non-interface functions.
163 #
164 # Example:
165 #
166 #   var some_view_handler = {
167 #           init   : func {},    # called only once at startup
168 #           start  : func {},    # called when view is switched to our view
169 #           stop   : func {},    # called when view is switched away from our view
170 #           reset  : func {},    # called with view.resetView()
171 #           update : func { 0 }, # called iteratively if defined. Must return
172 #   };                           # interval in seconds until next invocation
173 #                                # Don't define it if you don't need it!
174 #
175 #   view.manager.register("Some View", some_view_handler);
176 #
177 #
178 var manager = {
179         current : { node: nil, handler: default_handler },
180         init : func {
181                 me.views = {};
182                 me.loopid = 0;
183                 var viewnodes = props.globals.getNode("sim").getChildren("view");
184                 forindex (var i; viewnodes)
185                         me.views[i] = { node: viewnodes[i], handler: default_handler };
186                 setlistener("/sim/current-view/view-number", func(n) {
187                         manager.set_view(n.getValue());
188                 }, 1);
189         },
190         register : func(which, handler = nil) {
191                 if (num(which) == nil)
192                         which = view.indexof(which);
193                 if (handler == nil)
194                         handler = default_handler;
195                 me.views[which]["handler"] = handler;
196                 if (contains(handler, "init"))
197                         handler.init(me.views[which].node);
198                 me.set_view();
199         },
200         set_view : func(which = nil) {
201                 if (which == nil)
202                         which = getprop("/sim/current-view/view-number");
203                 elsif (num(which) == nil)
204                         which = view.indexof(which);
205
206                 me.loopid += 1;
207                 if (contains(me.current.handler, "stop"))
208                         me.current.handler.stop();
209
210                 me.current = me.views[which];
211
212                 if (contains(me.current.handler, "start"))
213                         me.current.handler.start();
214                 if (contains(me.current.handler, "update"))
215                         me._loop_(me.loopid += 1);
216         },
217         reset : func {
218                 if (contains(me.current.handler, "reset"))
219                         me.current.handler.reset();
220                 else
221                         default_handler.reset();
222         },
223         _loop_ : func(id) {
224                 id == me.loopid or return;
225                 settimer(func { me._loop_(id) }, me.current.handler.update());
226         },
227 };
228
229
230 ##
231 # View handler for fly-by view.
232 #
233 var fly_by_view_handler = {
234         init : func {
235                 me.latN = props.globals.getNode("/sim/viewer/latitude-deg", 1);
236                 me.lonN = props.globals.getNode("/sim/viewer/longitude-deg", 1);
237                 me.altN = props.globals.getNode("/sim/viewer/altitude-ft", 1);
238                 me.hdgN = props.globals.getNode("/orientation/heading-deg", 1);
239
240                 setlistener("/sim/signals/reinit", func(n) { n.getValue() or me.reset() });
241                 setlistener("/sim/crashed", func(n) { n.getValue() and me.reset() });
242                 setlistener("/sim/freeze/replay-state", func {
243                         settimer(func { me.reset() }, 1); # time for replay to catch up
244                 });
245                 me.reset();
246         },
247         start : func {
248                 me.reset();
249         },
250         reset: func {
251                 me.chase = -getprop("/sim/chase-distance-m");
252                 me.course = me.hdgN.getValue();
253                 me.last = geo.aircraft_position();
254                 me.setpos(1);
255                 me.dist = 20;
256         },
257         setpos : func(force = 0) {
258                 var pos = geo.aircraft_position();
259
260                 # check if the aircraft has moved enough
261                 var dist = me.last.distance_to(pos);
262                 if (dist < 1.7 * me.chase and !force)
263                         return 1.13;
264
265                 # "predict" and remember next aircraft position
266                 var course = me.hdgN.getValue();
267                 var delta_alt = (pos.alt() - me.last.alt()) * 0.5;
268                 pos.apply_course_distance(course, dist * 0.8);
269                 pos.set_alt(pos.alt() + delta_alt);
270                 me.last.set(pos);
271
272                 # apply random deviation
273                 var radius = me.chase * (0.5 * rand() + 0.7);
274                 var agl = getprop("/position/altitude-agl-ft") * geo.FT2M;
275                 if (agl > me.chase)
276                         var angle = rand() * 2 * math.pi;
277                 else
278                         var angle = ((2 * rand() - 1) * 0.15 + 0.5) * (rand() < 0.5 ? -math.pi : math.pi);
279
280                 var dev_alt = math.cos(angle) * radius;
281                 var dev_side = math.sin(angle) * radius;
282                 pos.apply_course_distance(course + 90, dev_side);
283
284                 # and make sure it's not under ground
285                 var lat = pos.lat();
286                 var lon = pos.lon();
287                 var alt = pos.alt();
288                 var elev = geo.elevation(lat, lon);
289                 if (elev != nil) {
290                         elev += 2;   # min elevation
291                         if (alt + dev_alt < elev and dev_alt < 0)
292                                 dev_alt = -dev_alt;
293                         if (alt + dev_alt < elev)
294                                 alt = elev;
295                         else
296                                 alt += dev_alt;
297                 }
298
299                 # set new view point
300                 me.latN.setValue(lat);
301                 me.lonN.setValue(lon);
302                 me.altN.setValue(alt * geo.M2FT);
303                 return 6.3;
304         },
305         update : func {
306                 return me.setpos();
307         },
308 };
309
310
311 #------------------------------------------------------------------------------
312 #
313 # Saves/restores/moves the view point (position, orientation, field-of-view).
314 # Moves are interpolated with sinusoidal characteristic. There's only one
315 # instance of this class, available as "view.point".
316 #
317 # Usage:
318 #    view.point.save();        ... save current view and return reference to
319 #                                  saved values in the form of a props.Node
320 #
321 #    view.point.restore();     ... restore saved view parameters
322 #
323 #    view.point.move(<prop> [, <time>]);
324 #                              ... set view parameters from a props.Node with
325 #                                  optional move time in seconds. <prop> may be
326 #                                  nil, in which case nothing happens.
327 #
328 # A parameter set as expected by set() and returned by save() is a props.Node
329 # object containing any (or none) of these children:
330 #
331 #   <heading-offset-deg>
332 #   <pitch-offset-deg>
333 #   <roll-offset-deg>
334 #   <x-offset-m>
335 #   <y-offset-m>
336 #   <z-offset-m>
337 #   <field-of-view>
338 #   <move-time-sec>
339 #
340 # The <move-time> isn't really a property of the view, but is available
341 # for convenience. The time argument in the move() method overrides it.
342
343
344 ##
345 # Normalize angle to  -180 <= angle < 180
346 #
347 var normdeg = func(a) {
348         while (a >= 180) {
349                 a -= 360;
350         }
351         while (a < -180) {
352                 a += 360;
353         }
354         return a;
355 }
356
357
358 ##
359 # Manages one translation/rotation axis. (For simplicity reasons the
360 # field-of-view parameter is also managed by this class.)
361 #
362 var ViewAxis = {
363         new : func(prop) {
364                 var m = { parents : [ViewAxis] };
365                 m.prop = props.globals.getNode(prop, 1);
366                 if (m.prop.getType() == "NONE") {
367                         m.prop.setDoubleValue(0);
368                 }
369                 m.from = m.to = m.prop.getValue();
370                 return m;
371         },
372         reset : func {
373                 me.from = me.to = normdeg(me.prop.getValue());
374         },
375         target : func(v) {
376                 me.to = normdeg(v);
377         },
378         move : func(blend) {
379                 me.prop.setValue(me.from + blend * (me.to - me.from));
380         },
381 };
382
383
384
385 ##
386 # view.point: handles smooth view movements
387 #
388 var point = {
389         init : func {
390                 me.axes = {
391                         "heading-offset-deg" : ViewAxis.new("/sim/current-view/goal-heading-offset-deg"),
392                         "pitch-offset-deg" : ViewAxis.new("/sim/current-view/goal-pitch-offset-deg"),
393                         "roll-offset-deg" : ViewAxis.new("/sim/current-view/goal-roll-offset-deg"),
394                         "x-offset-m" : ViewAxis.new("/sim/current-view/x-offset-m"),
395                         "y-offset-m" : ViewAxis.new("/sim/current-view/y-offset-m"),
396                         "z-offset-m" : ViewAxis.new("/sim/current-view/z-offset-m"),
397                         "field-of-view" : ViewAxis.new("/sim/current-view/field-of-view"),
398                 };
399                 me.storeN = props.Node.new();
400                 me.dtN = props.globals.getNode("/sim/time/delta-realtime-sec", 1);
401                 me.currviewN = props.globals.getNode("/sim/current-view", 1);
402                 me.blend = 0;
403                 me.loop_id = 0;
404                 props.copy(props.globals.getNode("/sim/view/config"), me.storeN);
405         },
406         save : func {
407                 me.storeN = props.Node.new();
408                 props.copy(me.currviewN, me.storeN);
409                 return me.storeN;
410         },
411         restore : func {
412                 me.move(me.storeN);
413         },
414         move : func(prop, time = nil) {
415                 prop != nil or return;
416                 foreach (var a; keys(me.axes)) {
417                         var n = prop.getNode(a);
418                         me.axes[a].reset();
419                         if (n != nil) {
420                                 me.axes[a].target(n.getValue());
421                         }
422                 }
423                 var m = prop.getNode("move-time-sec");
424                 if (m != nil) {
425                         time = m.getValue();
426                 }
427                 if (time == nil) {
428                         time = 1;
429                 }
430                 me.blend = -1;   # range -1 .. 1
431                 me._loop_(me.loop_id += 1, time);
432         },
433         _loop_ : func(id, time) {
434                 me.loop_id == id or return;
435                 me.blend += me.dtN.getValue() / time;
436                 if (me.blend > 1) {
437                         me.blend = 1;
438                 }
439                 var b = (math.sin(me.blend * math.pi / 2) + 1) / 2; # range 0 .. 1
440                 foreach (var a; keys(me.axes)) {
441                         me.axes[a].move(b);
442                 }
443                 if (me.blend < 1) {
444                         settimer(func { me._loop_(id, time) }, 0);
445                 }
446         },
447 };
448
449
450
451 var views = nil;
452 var fovProp = nil;
453
454
455 _setlistener("/sim/signals/nasal-dir-initialized", func {
456         views = props.globals.getNode("/sim").getChildren("view");
457         fovProp = props.globals.getNode("/sim/current-view/field-of-view");
458         point.init();
459 });
460
461
462 _setlistener("/sim/signals/fdm-initialized", func {
463         foreach (var v; views) {
464                 var index = v.getIndex();
465                 if (index > 6 and index < 100) {
466                         globals["view"] = nil;
467                         die("\n***\n*\n*  Illegal use of reserved view index "
468                                         ~ index ~ ". Use indices >= 100!\n*\n***");
469                 } elsif (index >= 100 and index < 200) {
470                         if (v.getNode("name") == nil)
471                                 continue;
472                         var e = v.getNode("enabled");
473                         if (e != nil) {
474                                 aircraft.data.add(e);
475                                 e.setAttribute("userarchive", 0);
476                         }
477                 }
478         }
479
480         manager.init();
481         manager.register("Fly-By View", fly_by_view_handler);
482 });
483
484