remove README.Protocol and add a README that refers to the "real"
[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 STEPS = 40;
18 ACUITY = 1/60; # Maximum angle subtended by one pixel (== 1 arc minute)
19 max = min = mul = 0;
20 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 increase = func {
30     calcMul();
31     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 decrease = func {
42     calcMul();
43     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 resetFOV = func {
52     setprop("/sim/current-view/field-of-view",
53             getprop("/sim/current-view/config/default-field-of-view-deg"));
54 }
55
56 ##
57 # Handler.  Reset view to default.
58 #
59 resetView = func {
60     if (getprop("/sim/current-view/view-number") == 6)
61         return flyby.setpos(1);
62
63     setprop("/sim/current-view/goal-heading-offset-deg",
64             getprop("/sim/current-view/config/heading-offset-deg"));
65     setprop("/sim/current-view/goal-pitch-offset-deg",
66             getprop("/sim/current-view/config/pitch-offset-deg"));
67     setprop("/sim/current-view/goal-roll-offset-deg",
68             getprop("/sim/current-view/config/roll-offset-deg"));
69     resetFOV();
70 }
71
72 ##
73 # Handler.  Step to the next view.
74 #
75 var stepView = func(n) {
76     var i = getprop("/sim/current-view/view-number") + n;
77     if (i < 0)
78         i = size(views) - 1;
79     elsif (i >= size(views))
80         i = 0;
81     setprop("/sim/current-view/view-number", i);
82
83     # And pop up a nice reminder
84     gui.popupTip(views[i].getNode("name").getValue());
85 }
86
87 ##
88 # Standard view "slew" rate, in degrees/sec.
89 #
90 VIEW_PAN_RATE = 60;
91
92 ##
93 # Pans the view horizontally.  The argument specifies a relative rate
94 # (or number of "steps" -- same thing) to the standard rate.
95 #
96 panViewDir = func(step) {
97     if (getprop("/sim/freeze/master"))
98         prop = "/sim/current-view/heading-offset-deg";
99     else
100         prop = "/sim/current-view/goal-heading-offset-deg";
101
102     controls.slewProp(prop, step * VIEW_PAN_RATE);
103 }
104
105 ##
106 # Pans the view vertically.  The argument specifies a relative rate
107 # (or number of "steps" -- same thing) to the standard rate.
108 #
109 panViewPitch = func(step) {
110     if (getprop("/sim/freeze/master"))
111         prop = "/sim/current-view/pitch-offset-deg";
112     else
113         prop = "/sim/current-view/goal-pitch-offset-deg";
114
115     controls.slewProp(prop, step * VIEW_PAN_RATE);
116 }
117
118
119
120 ##
121 # Singleton class that manages "Fly-By View". It's started with flyby.init()
122 # and then works autonomously.
123 #
124 var flyby = {
125     init : func {
126         me.latN = props.globals.getNode("/sim/viewer/latitude-deg", 1);
127         me.lonN = props.globals.getNode("/sim/viewer/longitude-deg", 1);
128         me.altN = props.globals.getNode("/sim/viewer/altitude-ft", 1);
129         me.hdgN = props.globals.getNode("/orientation/heading-deg", 1);
130         me.loopid = 0;
131         me.number = nil;
132         me.currview = nil;
133         forindex (var i; views)
134             if ((var v = views[i].getNode("name")) != nil and v.getValue() == "Fly-By View")
135                 me.number = i;
136         if (me.number == nil)
137             die("can't find 'Fly-By View'");
138
139         setlistener("/sim/signals/reinit", func { cmdarg().getValue() or me.reset() });
140         setlistener("/sim/crashed", func { cmdarg().getValue() and me.reset() });
141         setlistener("/sim/freeze/replay-state", func {
142             settimer(func { me.reset() }, 1); # time for replay to catch up
143         });
144         setlistener("/sim/current-view/view-number", func {
145             me.currview = cmdarg().getValue();
146             me.reset();
147         }, 1);
148     },
149     reset: func {
150         me.loopid += 1;
151         me.currview == me.number or return;
152         me.chase = -getprop("/sim/chase-distance-m");
153         me.course = me.hdgN.getValue();
154         me.last = geo.aircraft_position();
155         me.setpos(1);
156         me.dist = 20;
157         me._loop_(me.loopid);
158     },
159     setpos : func(force = 0) {
160         var pos = geo.aircraft_position();
161
162         # check if the aircraft has moved enough
163         var dist = me.last.distance_to(pos);
164         if (dist < 1.7 * me.chase and !force)
165             return 1.13;
166
167         # "predict" and remember next aircraft position
168         var course = me.hdgN.getValue();
169         var delta_alt = (pos.alt() - me.last.alt()) * 0.5;
170         pos.apply_course_distance(course, dist * 0.8);
171         pos.set_alt(pos.alt() + delta_alt);
172         me.last.set(pos);
173
174         # apply random deviation
175         var radius = me.chase * (0.5 * rand() + 0.7);
176         var agl = getprop("/position/altitude-agl-ft") * geo.FT2M;
177         if (agl > me.chase)
178             var angle = rand() * 2 * math.pi;
179         else
180             var angle = ((2 * rand() - 1) * 0.15 + 0.5) * (rand() < 0.5 ? -math.pi : math.pi);
181
182         var dev_alt = math.cos(angle) * radius;
183         var dev_side = math.sin(angle) * radius;
184         pos.apply_course_distance(course + 90, dev_side);
185
186         # and make sure it's not under ground
187         var lat = pos.lat();
188         var lon = pos.lon();
189         var alt = pos.alt();
190         var elev = geo.elevation(lat, lon);
191         if (elev != nil) {
192             elev += 2;   # min elevation
193             if (alt + dev_alt < elev and dev_alt < 0)
194                 dev_alt = -dev_alt;
195             if (alt + dev_alt < elev)
196                 alt = elev;
197             else
198                 alt += dev_alt;
199         }
200
201         # set new view point
202         me.latN.setValue(lat);
203         me.lonN.setValue(lon);
204         me.altN.setValue(alt * geo.M2FT);
205         return 6.3;
206     },
207     _loop_ : func(id) {
208         id == me.loopid or return;
209         settimer(func { me._loop_(id) }, me.setpos());
210     },
211 };
212
213
214
215 #-- view manager --------------------------------------------------------------
216 #
217 # Saves/restores/moves the view point (position, orientation, field-of-view).
218 # Moves are interpolated with sinusoidal characteristic. There's only one
219 # instance of this class, available as "view.point".
220 #
221 # Usage:
222 #    view.point.save();        ... save current view and return reference to
223 #                                  saved values in the form of a props.Node
224 #
225 #    view.point.restore();     ... restore saved view parameters
226 #
227 #    view.point.move(<prop> [, <time>]);
228 #                              ... set view parameters from a props.Node with
229 #                                  optional move time in seconds. <prop> may be
230 #                                  nil, in which case nothing happens.
231 #
232 # A parameter set as expected by set() and returned by save() is a props.Node
233 # object containing any (or none) of these children:
234 #
235 #   <heading-offset-deg>
236 #   <pitch-offset-deg>
237 #   <roll-offset-deg>
238 #   <x-offset-m>
239 #   <y-offset-m>
240 #   <z-offset-m>
241 #   <field-of-view>
242 #   <move-time-sec>
243 #
244 # The <move-time> isn't really a property of the view, but is available
245 # for convenience. The time argument in the move() method overrides it.
246
247
248 ##
249 # Normalize angle to  -180 <= angle < 180
250 #
251 var normdeg = func(a) {
252     while (a >= 180) {
253         a -= 360;
254     }
255     while (a < -180) {
256         a += 360;
257     }
258     return a;
259 }
260
261
262 ##
263 # Manages one translation/rotation axis. (For simplicity reasons the
264 # field-of-view parameter is also managed by this class.)
265 #
266 var ViewAxis = {
267     new : func(prop) {
268         var m = { parents : [ViewAxis] };
269         m.prop = props.globals.getNode(prop, 1);
270         if (m.prop.getType() == "NONE") {
271             m.prop.setDoubleValue(0);
272         }
273         m.from = m.to = m.prop.getValue();
274         return m;
275     },
276     reset : func {
277         me.from = me.to = normdeg(me.prop.getValue());
278     },
279     target : func(v) {
280         me.to = normdeg(v);
281     },
282     move : func(blend) {
283         me.prop.setValue(me.from + blend * (me.to - me.from));
284     },
285 };
286
287
288
289 ##
290 # view.point: handles smooth view movements
291 #
292 var point = {
293     init : func {
294         me.axes = {
295             "heading-offset-deg" : ViewAxis.new("/sim/current-view/goal-heading-offset-deg"),
296             "pitch-offset-deg" : ViewAxis.new("/sim/current-view/goal-pitch-offset-deg"),
297             "roll-offset-deg" : ViewAxis.new("/sim/current-view/goal-roll-offset-deg"),
298             "x-offset-m" : ViewAxis.new("/sim/current-view/x-offset-m"),
299             "y-offset-m" : ViewAxis.new("/sim/current-view/y-offset-m"),
300             "z-offset-m" : ViewAxis.new("/sim/current-view/z-offset-m"),
301             "field-of-view" : ViewAxis.new("/sim/current-view/field-of-view"),
302         };
303         me.storeN = props.Node.new();
304         me.dtN = props.globals.getNode("/sim/time/delta-realtime-sec", 1);
305         me.currviewN = props.globals.getNode("/sim/current-view", 1);
306         me.blend = 0;
307         me.loop_id = 0;
308         props.copy(props.globals.getNode("/sim/view/config"), me.storeN);
309     },
310     save : func {
311         me.storeN = props.Node.new();
312         props.copy(me.currviewN, me.storeN);
313         return me.storeN;
314     },
315     restore : func {
316         me.move(me.storeN);
317     },
318     move : func(prop, time = nil) {
319         prop != nil or return;
320         foreach (var a; keys(me.axes)) {
321             var n = prop.getNode(a);
322             me.axes[a].reset();
323             if (n != nil) {
324                 me.axes[a].target(n.getValue());
325             }
326         }
327         var m = prop.getNode("move-time-sec");
328         if (m != nil) {
329             time = m.getValue();
330         }
331         if (time == nil) {
332             time = 1;
333         }
334         me.blend = -1;   # range -1 .. 1
335         me._loop_(me.loop_id += 1, time);
336     },
337     _loop_ : func(id, time) {
338         me.loop_id == id or return;
339         me.blend += me.dtN.getValue() / time;
340         if (me.blend > 1) {
341             me.blend = 1;
342         }
343         var b = (math.sin(me.blend * math.pi / 2) + 1) / 2; # range 0 .. 1
344         foreach (var a; keys(me.axes)) {
345             me.axes[a].move(b);
346         }
347         if (me.blend < 1) {
348             settimer(func { me._loop_(id, time) }, 0);
349         }
350     },
351 };
352
353
354
355 var views = nil;
356 var fovProp = nil;
357
358
359 _setlistener("/sim/signals/nasal-dir-initialized", func {
360     views = props.globals.getNode("/sim").getChildren("view");
361     fovProp = props.globals.getNode("/sim/current-view/field-of-view");
362     point.init();
363 });
364
365
366 _setlistener("/sim/signals/fdm-initialized", func {
367     flyby.init();
368 });
369
370