Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / webgui / main.js
1 require.config({
2     baseUrl : '.',
3     paths : {
4         jquery : '3rdparty/jquery/jquery-1.11.2.min',
5         'jquery-ui' : '3rdparty/jquery/ui',
6         knockout : '3rdparty/knockout/knockout-3.2.0',
7         kojqui : '3rdparty/knockout-jqueryui',
8         sprintf : '3rdparty/sprintf/sprintf.min',
9         leaflet : '3rdparty/leaflet-0.7.3/leaflet',
10         text : '3rdparty/require/text',
11         flot : '3rdparty/flot/jquery.flot',
12         flotresize : '3rdparty/flot/jquery.flot.resize',
13         flottime : '3rdparty/flot/jquery.flot.time',
14         fgcommand : 'lib/fgcommand',
15         sammy: '3rdparty/sammy-latest.min'
16     }
17 });
18
19 require([
20         'knockout', 'jquery','sammy',  'themeswitch', 'kojqui/button', 'flot', 'leaflet'
21 ], function(ko, jquery, Sammy) {
22
23     function KnockProps(aliases) {
24
25         var self = this;
26
27         self.initWebsocket = function() {
28             self.ws = new WebSocket('ws://' + location.host + '/PropertyListener');
29
30             self.ws.onclose = function(ev) {
31                 var msg = 'Lost connection to FlightGear. Should I try to reconnect?';
32                 if (confirm(msg)) {
33                     // try reconnect
34                     self.initWebsocket();
35                 } else {
36                     throw new Error(msg);
37                 }
38             }
39
40             self.ws.onerror = function(ev) {
41                 var msg = 'Error communicating with FlightGear. Please reload this page and/or restart FlightGear.';
42                 alert(msg);
43                 throw new Error(msg);
44             }
45
46             self.ws.onmessage = function(ev) {
47                 try {
48                     self.fire(JSON.parse(ev.data));
49                 } catch (e) {
50                 }
51             };
52
53             self.openCache = [];
54             self.ws.onopen = function(ev) {
55                 // send subscriptions when the socket is open
56                 var c = self.openCache;
57                 delete self.openCache;
58                 c.forEach(function(e) {
59                     self.addListener(e.prop, e.koObservable);
60                 });
61                 for ( var p in self.listeners) {
62                     self.addListener(p, self.listeners[p]);
63                 }
64             }
65
66         }
67
68         self.initWebsocket();
69
70         self.fire = function(json) {
71             var value = json.value;
72             var listeners = self.listeners[json.path] || [];
73             listeners.forEach(function(koObservable) {
74                 koObservable(value)
75             });
76             koObservable(json.value);
77         }
78
79         function resolvePropertyPath(self, pathOrAlias) {
80             if (pathOrAlias in self.aliases)
81                 return self.aliases[pathOrAlias];
82             if (pathOrAlias.charAt(0) == '/')
83                 return pathOrAlias;
84             return null;
85         }
86
87         self.listeners = {}
88
89         self.removeListener = function(pathOrAlias, koObservable) {
90             var path = resolvePropertyPath(self, pathOrAlias);
91             if (path == null) {
92                 console.log("can't remove listener for " + pathOrAlias + ": unknown alias or invalid path.");
93                 return self;
94             }
95
96             var listeners = self.listeners[path] || [];
97             var idx = listeners.indexOf(koObservable);
98             if (idx == -1) {
99                 console.log("can't remove listener for " + path + ": not a listener.");
100                 return self;
101             }
102
103             listeners.splice(idx, 1);
104
105             if (0 == listeners.length) {
106                 self.ws.send(JSON.stringify({
107                     command : 'removeListener',
108                     node : path
109                 }));
110             }
111
112             return self;
113         }
114
115         self.addListener = function(alias, koObservable) {
116             if (self.openCache) {
117                 // socket not yet open, just cache the request
118                 self.openCache.push({
119                     "prop" : alias,
120                     "koObservable" : koObservable
121                 });
122                 return self;
123             }
124
125             var path = resolvePropertyPath(self, alias);
126             if (path == null) {
127                 console.log("can't listen to " + alias + ": unknown alias or invalid path.");
128                 return self;
129             }
130
131             var listeners = self.listeners[path] = (self.listeners[path] || []);
132             if (listeners.indexOf(koObservable) != -1) {
133                 console.log("won't listen to " + path + ": duplicate.");
134                 return self;
135             }
136
137             listeners.push(koObservable);
138
139             if (1 == listeners.length) {
140                 self.ws.send(JSON.stringify({
141                     command : 'addListener',
142                     node : path
143                 }));
144             }
145             self.ws.send(JSON.stringify({
146                 command : 'get',
147                 node : path
148             }));
149
150             return self;
151         }
152
153         self.aliases = aliases || {};
154         self.setAliases = function(arg) {
155             arg.forEach(function(a) {
156                 self.aliases[a[0]] = a[1];
157             });
158         }
159
160         self.props = {};
161
162         self.get = function(target, prop) {
163             if (self.props[prop]) {
164                 return self.props[prop];
165             }
166
167             var p = (self.props[prop] = ko.pureComputed({
168                 read : target,
169                 write : function(newValue) {
170                     if (newValue == target())
171                         return;
172                     target(newValue);
173                     target.notifySubscribers(newValue);
174                 }
175             }));
176
177             self.addListener(prop, p);
178
179             return p;
180         }
181
182         self.write = function(prop, value) {
183             var path = this.aliases[prop] || "";
184             if (path.length == 0) {
185                 console.log("can't write " + prop + ": unknown alias.");
186                 return;
187             }
188             this.ws.send(JSON.stringify({
189                 command : 'set',
190                 node : path,
191                 value : value
192             }));
193         }
194
195         self.propsToObject = function(prop, map, result) {
196             result = result || {}
197             prop.children.forEach(function(prop) {
198                 var target = map[prop.name] || null;
199                 if (target) {
200                     if (typeof (result[target]) === 'function') {
201                         result[target](prop.value);
202                     } else {
203                         result[target] = prop.value;
204                     }
205                 }
206             });
207             return result;
208         }
209     }
210
211     ko.extenders.fgprop = function(target, prop) {
212         return ko.utils.knockprops.get(target, prop);
213     };
214
215     ko.utils.knockprops = new KnockProps();
216
217     ko.utils.knockprops.setAliases([
218             // time
219             [
220                     "gmt", "/sim/time/gmt"
221             ], [
222                     "local-offset", "/sim/time/local-offset"
223             ],
224
225             // flight
226             [
227                     "pitch", "/orientation/pitch-deg"
228             ], [
229                     "roll", "/orientation/roll-deg"
230             ], [
231                     "heading", "/orientation/heading-magnetic-deg"
232             ], [
233                     "true-heading", "/orientation/heading-deg"
234             ], [
235                     "altitude", "/position/altitude-ft"
236             ], [
237                     "latitude", "/position/latitude-deg"
238             ], [
239                     "longitude", "/position/longitude-deg"
240             ], [
241                     "airspeed", "/velocities/airspeed-kt"
242             ], [
243                     "groundspeed", "/velocities/groundspeed-kt"
244             ], [
245                     "slip", "/instrumentation/slip-skid-ball/indicated-slip-skid"
246             ], [
247                     "cg", "/fdm/jsbsim/inertia/cg-x-in"
248             ], [
249                     "weight", "/fdm/jsbsim/inertia/weight-lbs"
250             ],
251             // radio settings
252             [
253                     "com1stn", "/instrumentation/comm/station-name"
254             ], [
255                     "com1use", "/instrumentation/comm/frequencies/selected-mhz"
256             ], [
257                     "com1sby", "/instrumentation/comm/frequencies/standby-mhz"
258             ], [
259                     "com1stn", "/instrumentation/comm/station-name"
260             ], [
261                     "com2stn", "/instrumentation/comm[1]/station-name"
262             ], [
263                     "com2use", "/instrumentation/comm[1]/frequencies/selected-mhz"
264             ], [
265                     "com2sby", "/instrumentation/comm[1]/frequencies/standby-mhz"
266             ], [
267                     "com2stn", "/instrumentation/comm[1]/station-name"
268             ], [
269                     "nav1use", "/instrumentation/nav/frequencies/selected-mhz"
270             ], [
271                     "nav1sby", "/instrumentation/nav/frequencies/standby-mhz"
272             ], [
273                     "nav1stn", "/instrumentation/nav/nav-id"
274             ], [
275                     "nav2use", "/instrumentation/nav[1]/frequencies/selected-mhz"
276             ], [
277                     "nav2sby", "/instrumentation/nav[1]/frequencies/standby-mhz"
278             ], [
279                     "nav2stn", "/instrumentation/nav[1]/nav-id"
280             ], [
281                     "adf1use", "/instrumentation/adf/frequencies/selected-khz"
282             ], [
283                     "adf1sby", "/instrumentation/adf/frequencies/standby-khz"
284             ], [
285                     "adf1stn", "/instrumentation/adf/ident"
286             ], [
287                     "dme1use", "/instrumentation/dme/frequencies/selected-mhz"
288             ], [
289                     "dme1dst", "/instrumentation/dme/indicated-distance-nm"
290             ], [
291                     "xpdrcod", "/instrumentation/transponder/id-code"
292             ],
293             // weather
294             [
295                     "ac-wdir", "/environment/wind-from-heading-deg"
296             ], [
297                     "ac-wspd", "/environment/wind-speed-kt"
298             ], [
299                     "ac-visi", "/environment/visibility-m"
300             ], [
301                     "ac-temp", "/environment/temperature-degc"
302             ], [
303                     "ac-dewp", "/environment/dewpoint-degc"
304             ], [
305                     "gnd-wdir", "/environment/config/boundary/entry/wind-from-heading-deg"
306             ], [
307                     "gnd-wspd", "/environment/config/boundary/entry/wind-speed-kt"
308             ], [
309                     "gnd-visi", "/environment/config/boundary/entry/visibility-m"
310             ], [
311                     "gnd-temp", "/environment/config/boundary/entry/temperature-degc"
312             ], [
313                     "gnd-dewp", "/environment/config/boundary/entry/dewpoint-degc"
314             ], [
315                     "metar-valid", "/environment/metar/valid"
316             ],
317     ]);
318
319     function PhiViewModel(props) {
320         var self = this;
321         self.props = props;
322         self.widgets = ko.observableArray([
323                 "metar", "efis", "radiostack", "map"
324         ]);
325
326         self.topics = [
327                 'Aircraft', 'Environment', 'Map', 'Tools', 'Simulator', 'Help',
328         ];
329
330         self.selectedTopic = ko.observable();
331         self.selectedSubtopic = ko.observable();
332
333         self.selectTopic = function(topic) {
334             location.hash = topic;
335         }
336
337         self.refresh = function() {
338             location.reload();
339         }
340
341         // Client-side routes
342         Sammy(function() {
343             this.get('#:topic', function() {
344                 self.selectedTopic( this.params.topic );
345                 self.selectedSubtopic('');
346             });
347
348             this.get('#:topic/:subtopic', function() {
349                 self.selectedTopic( this.params.topic );
350                 self.selectedSubtopic( this.params.subtopic );
351             });
352             // empty route
353             this.get('', function() {
354                 this.app.runRoute( 'get', '#' + self.topics[0] );
355             });
356         }).run();
357     }
358
359     ko.components.register('Aircraft', {
360         require : 'topics/Aircraft'
361     });
362
363     ko.components.register('Environment', {
364         require : 'topics/Environment'
365     });
366
367     ko.components.register('Map', {
368         require : 'topics/Map'
369     });
370
371     ko.components.register('Tools', {
372         require : 'topics/Tools'
373     });
374
375     ko.components.register('Simulator', {
376         require : 'topics/Simulator'
377     });
378
379     ko.components.register('Help', {
380         require : 'topics/Help'
381     });
382
383     ko.components.register('map', {
384         require : 'widgets/map'
385     });
386
387     ko.components.register('radiostack', {
388         require : 'widgets/radiostack'
389     });
390
391     ko.components.register('metar', {
392         require : 'widgets/metar'
393     });
394
395     ko.components.register('efis', {
396         require : 'widgets/efis'
397     });
398
399     ko.components.register('stopwatch', {
400         require : 'widgets/Stopwatch'
401     });
402
403     ko.bindingHandlers.flotchart = {
404         init : function(element, valueAccessor, allBindings) {
405             // This will be called when the binding is first applied to an
406             // element
407             // Set up any initial state, event handlers, etc. here
408             var value = valueAccessor() || {};
409
410             if (value.hover && typeof (value.hover) === 'function') {
411                 $(element).bind("plothover", function(event, pos, item) {
412                     value.hover(pos, item);
413                 });
414             }
415         },
416
417         update : function(element, valueAccessor, allBindings) {
418             var value = valueAccessor() || {};
419             var data = ko.unwrap(value.data);
420             var options = ko.unwrap(value.options);
421             jquery.plot(element, data, options);
422
423         },
424
425     };
426
427     ko.applyBindings(new PhiViewModel(),document.getElementById('wrapper'));
428
429 });