Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / canvas / map.nas
1 ###
2 # map.nas -     provide a high level method to create typical maps in FlightGear (airports, navaids, fixes and waypoints) for both, the GUI and instruments
3 #               implements the notion of a "layer" by using canvas groups and adding geo-referenced elements to a layer
4 #               layered maps are linked to boolean properties so that visibility can be easily toggled (via GUI checkboxes or cockpit hotspots)
5 #               without having to redraw other layers
6 #
7 # GOALS:        have a single Nasal/Canvas wrapper for all sort of maps in FlightGear, that can be easily shared and reused for different purposes
8 #
9 # DESIGN:       ... is slowly evolving, but still very much beta for the time being
10 #
11 # API:          not yet documented, but see eventually design.txt (will need to add doxygen-strings then)
12 #
13 # PERFORMANCE:  will be improved, probabaly by moving some features to C++ space and optimizing things there
14 #
15 #
16 # ISSUES:       just look for the FIXME and TODO strings - currently, the priority is to create an OOP/MVC design with less specialized code in XML files
17 #
18 #
19 # REGRESSIONS:  744 ND: toggle layer on/off, support different dialogs
20 #
21 # ROADMAP:      Generalize this further, so that:
22 #
23 #                       - it can be easily reused
24 #                       - it uses a MVC approach, where layer-specific data is provided by a Model object
25 #                       - other dialogs can use this without tons of custom code (airports.xml, route-manager.xml, map-canvas.xml)
26 #                       - generalize this further so that it can be used by MFDs/instruments
27 #                       - implement additional layers (tcas, wxradar, agradar) - especially expose the required data to Nasal
28 #                       - implement better GUI support (events) so that zooming/panning via mouse can be supported
29 #                       - make the whole thing styleable
30 #
31 #                       - keep track of things getting added here and decide if they should better move to the core canvas module or the C++ code
32 #
33 #
34 # C++ RFEs:
35 #               - overload findNavaidsWithinRange() to support an optional position argument, so that arbitrary navaids can be looked up
36 #               - add Nasal extension function to get scenery vector data (landclass)
37 #               -
38 #               -
39 #
40
41
42 #FIXME: this is a hack so that dialogs can register their own
43 # callbacks that are automatically invoked at the end of the
44 # generic-canvas-map.xml file (canvas/nasal section)
45 var callbacks = [];
46 var register_callback = func(c) append(callbacks, c);
47 var run_callbacks = func foreach(var c; callbacks) c();
48
49 var DEBUG=0;
50 if (DEBUG) {
51         var benchmark = debug.benchmark;
52 } else {
53         var benchmark = func(label, code) code(); # NOP
54 }
55
56 var assert = func(label, expr) expr and die(label);
57
58 # Mapping from surface codes to colors (shared by runways.draw and taxiways.draw)
59 var SURFACECOLORS = {
60         1 : { type: "asphalt",  r:0.2,  g:0.2, b:0.2 },
61         2 : { type: "concrete", r:0.3,  g:0.3, b:0.3 },
62         3 : { type: "turf",     r:0.2,  g:0.5, b:0.2 },
63         4 : { type: "dirt",     r:0.4,  g:0.3, b:0.3 },
64         5 : { type: "gravel",   r:0.35, g:0.3, b:0.3 },
65 #  Helipads
66         6 : { type: "asphalt",  r:0.2,  g:0.2, b:0.2 },
67         7 : { type: "concrete", r:0.3,  g:0.3, b:0.3 },
68         8 : { type: "turf",     r:0.2,  g:0.5, b:0.2 },
69         9 : { type: "dirt",     r:0.4,  g:0.3, b:0.3 },
70         0 : { type: "gravel",   r:0.35, g:0.3, b:0.3 },
71 };
72
73
74 ###
75 # ALL LayeredMap "draws" go through this wrapper, which makes it easy to check what's going on:
76 var draw_layer = func(layer, callback, lod) {
77         var name= layer._view.get("id");
78         # print("Canvas:Draw op triggered"); # just to make sure that we are not adding unnecessary data when checking/unchecking a checkbox
79         #if (DEBUG and name=="taxiways") fgcommand("profiler-start"); #without my patch, this is a no op, so no need to disable
80         #print("Work items:", size(layer._model._elements));
81         benchmark("Drawing Layer:"~layer._view.get("id"), func
82         foreach(var element; layer._model._elements) {
83                 #print(typeof(layer._view));
84                 #debug.dump(layer._view);
85                 callback(layer._view, element, layer._controller, lod); # ISSUE here
86         });
87         if (! layer._model.hasData() ) print("Layer was EMPTY:", name);
88         #if (DEBUG and name=="taxiways") fgcommand("profiler-stop");
89         layer._drawn=1; #TODO: this should be encapsulated
90 }
91
92 # Runway
93 #
94 var Runway = {
95         # Create Runway from hash
96         #
97         # @param rwy  Hash containing runway data as returned from
98         #             airportinfo().runways[ <runway designator> ]
99         new: func(rwy) {
100                 return {
101                         parents: [Runway],
102                         rwy: rwy
103                 };
104         },
105         # Get a point on the runway with the given offset
106         #
107         # @param pos  Position along the center line
108         # @param off  Offset perpendicular to the center line
109         pointOffCenterline: func(pos, off = 0) {
110                 var coord = geo.Coord.new();
111                 coord.set_latlon(me.rwy.lat, me.rwy.lon);
112                 coord.apply_course_distance(me.rwy.heading, pos);
113
114                 if(off)
115                         coord.apply_course_distance(me.rwy.heading + 90, off);
116
117                 return ["N" ~ coord.lat(), "E" ~ coord.lon()];
118         }
119 };
120
121 var make = func return {parents:arg};
122
123 ##
124 # A layer model is just a wrapper for a vector with elements
125 # either updated via a timer or via a listener (or both)
126
127 var LayerModel = {_elements:[], _view:, _controller:{query_range:func 100}, };
128 LayerModel.new = func make(LayerModel);
129 LayerModel.clear = func me._elements = [];
130 LayerModel.push = func (e) append(me._elements, e);
131 LayerModel.get = func me._elements;
132 LayerModel.update = func;
133 LayerModel.hasData = func size(me. _elements);
134 LayerModel.setView = func(v) me._view=v;
135 LayerModel.setController = func(c) me._controller=c;
136
137
138 ##
139 # A layer is mapped to a canvas group
140 # Layers are linked to a single boolean property to toggle them on/off
141 ## FIXME: this is GUI specific ATM
142 var Layer = {
143         _model: ,
144         _view:  ,
145         _controller: ,
146         _drawn:0,
147 };
148
149 Layer.new = func(group, name, model, controller=nil) {
150         #print("Setting up new Layer:", name);
151         var m = make(Layer);
152         m._model = model.new();
153         if (controller!=nil) {
154           m._controller = controller;
155           m._model._controller = controller;
156         }
157         else # use the default controller (query_range for positioned queries =100nm)
158         m._controller = m._model._controller;
159
160         #print("Model name is:", m._model.name);
161         m._view =       group.createChild("group",name);
162         m._model._view = m;
163         m.name = name; #FIXME: not needed, there's already _view.get("id")
164         return m;
165 }
166
167 Layer.hide = func me._view.setVisible(0);
168 Layer.show = func me._view.setVisible(1);
169 #TODO: Unify toggle and update methods - and support lazy drawing (make it optional!)
170 Layer.toggle = func {
171         # print("Toggling layer");
172         var checkbox = getprop(me.display_layer);
173         if(checkbox and !me._drawn) {
174                 # print("Lazy drawing");
175                 me.draw();
176         }
177
178         #var state= me._view.getBool("visible");
179         #print("Toggle layer visibility ",me.display_layer," checkbox is", checkbox);
180         #print("Layer id is:", me._view.get("id"));
181         #print("Drawn is:", me._drawn);
182         checkbox?me._view.setVisible(1) : me._view.setVisible(0);
183 }
184 Layer.reset = func {
185         me._view.removeAllChildren(); # clear the "real" canvas drawables
186         me._model.clear(); # the vector is used for lazy rendering
187         assert("Model not emptied during layer reset!", me._model.hasData() );
188         me._drawn = 0;
189 }
190 #TODO: Unify toggle and update FIXME: GUI specific, not needed for 744 ND.nas
191 Layer.update = func {
192         # print("Layer update: Check if layer is visible, if so, draw");
193         if (contains(me, "display_layer")) #UGLY HACK
194         if (! getprop(me.display_layer)) return; # checkbox for layer not set
195
196         if (!me._model.hasData() ) return; # no data available
197         # print("Trying to draw");
198         me.draw();
199 }
200
201 Layer.setDraw = func(callback) me.draw = callback;
202 Layer.setController = func(c) me._controller=c; # TODO: implement
203 Layer.setModel = func(m) nil; # TODO: implement
204
205
206
207 ##
208 # A layered map consists of several layers
209 # TODO: Support nested LayeredMaps, where a LayeredMap may contain other LayeredMaps
210 # TODO: use MapBehavior here and move the zoom/refpos methods there, so that map behavior can be easily customized
211 var LayeredMap = {
212         ranges:[],
213         zoom_property:nil, listeners:[],
214         update_property:nil, layers:[],
215 };
216 LayeredMap.new = func(parent, name)
217         return make(LayeredMap, parent.createChild("map",name) );
218
219 LayeredMap.listen = func(p,c) { #FIXME: listening should be managed by each m/v/c separately
220         # print("Setting up LayeredMap-managed listener:", p);
221         append(me.listeners, setlistener(p, c));
222 }
223
224 LayeredMap.initializeLayers = func {
225         # print("initializing all layers and updating");
226         foreach(var l; me.layers)
227                 l.update();
228 }
229
230 LayeredMap.setRefPos = func(lat, lon) {
231         # print("RefPos set");
232         me._node.getNode("ref-lat", 1).setDoubleValue(lat);
233         me._node.getNode("ref-lon", 1).setDoubleValue(lon);
234         me; # chainable
235 }
236 LayeredMap.setHdg = func(hdg) {
237         me._node.getNode("hdg",1).setDoubleValue(hdg);
238         me; # chainable
239 }
240
241 LayeredMap.updateZoom = func {
242         var z = me.zoom_property.getValue() or 0;
243         z = math.max(0, math.min(z, size(me.ranges) - 1));
244         me.zoom_property.setIntValue(z);
245         var zoom = me.ranges[size(me.ranges) - 1 - z];
246         # print("Setting zoom range to:", zoom);
247         benchmark("Zooming map:"~zoom, func {
248                 me._node.getNode("range", 1).setDoubleValue(zoom);
249                 # TODO update center/limit translation to keep airport always visible
250         });
251         me; #chainable
252 }
253
254 # this is a huge hack at the moment, we need to encapsulate the setRefPos/setHdg methods, so that they are exposed to XML space
255 #
256 LayeredMap.updateState = func {
257         # center map on airport TODO: should be moved to a method and wrapped with a controller so that behavior can be customized
258         #var apt = me.layers[0]._model._elements[0];
259         # FIXME:
260         #me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon);
261
262         me.setHdg(0.0);
263         me.updateZoom();
264 }
265
266 #
267 # TODO: this is currently GUI specific and not re-usable for instruments
268 LayeredMap.setupZoom = func(dialog) {
269         var dlgroot =  dialog.getNode("features/dialog-root").getValue();#FIXME: GUI specific - needs to be re-implemented for instruments
270         me.zoom_property = props.globals.getNode(dlgroot ~"/"~dialog.getNode("features/range-property").getValue(), 1); #FIXME: this doesn't belong here, need to be in ctor instead !!!
271         ranges=dialog.getNode("features/ranges").getChildren("range");
272         if( size(me.ranges) == 0 )
273                 # TODO check why this gets called everytime the dialog is opened
274                 foreach(var r; ranges)
275                         append(me.ranges, r.getValue() );
276
277         # print("Setting up Zoom Ranges:", size(ranges)-1);
278         me.listen(me.zoom_property, func me.updateZoom() );
279         me.updateZoom();
280         me; #chainable
281 }
282 LayeredMap.setZoom = func {} #TODO
283
284 LayeredMap.resetLayers = func {
285         benchmark("Resetting LayeredMap",
286                 func foreach(var l; me.layers) { #TODO: hide all layers, hide map
287                         l.reset();
288                 }
289         );
290 }
291
292 #FIXME: listener management should be done at the MVC level, for each component - not as part of the LayeredMap!
293 LayeredMap.cleanup_listeners = func {
294         # print("Cleaning up listeners");
295         foreach(var l; me.listeners)
296                 removelistener(l);
297         # TODO check why me.listeners = []; doesn't work. Maybe this is a Nasal bug
298         #      and the old vector is somehow used again.
299         setsize(me.listeners, 0);
300 }
301
302 ###
303 # GenericMap: A generic map is a layered map that puts all supported features on a different layer (canvas group) so that
304 # they can be individually toggled on/off so that unnecessary updates are avoided, there are methods to link layers to boolean properties
305 # so that they can be easily associated with GUI properties (checkboxes) or cockpit hotspots
306 # TODO: generalize the XML-parametrization and move it to a helper class
307
308 var GenericMap = { };
309 GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap);
310
311 GenericMap.setupLayer = func(layer, property) {
312         var l = MAP_LAYERS[layer].new(me, layer,nil); # Layer.new(me, layer);
313         l.display_layer = property; #FIXME: use controller object instead here and this overlaps with update_property
314         #print("Set up layer with toggle property=", property);
315         l._view.setVisible( getprop(property) ) ;
316         append(me.layers, l);
317         return l;
318 }
319
320 # features are layers - so this will do layer setup and then register listeners for each layer
321 GenericMap.setupFeature = func(layer, property, init ) {
322         var l=me.setupLayer( layer, property );
323         me.listen(property, func l.toggle() );  #TODO: should use the controller object here !
324
325         l._model._update_property=property; #TODO: move somewhere else - this is the property that is mapped to the CHECKBOX
326         l._model._view = l; #FIXME: very crude, set a handle to the view(group), so that the model can notify it (for updates)
327         l._model._map = me; #FIXME: added here so that layers can send update requests to the parent map
328         #print("Setting up layer init for property:", init);
329
330         l._model._input_property = init; # FIXME: init property = input property - needs to be improved!
331         me.listen(init, func l._model.init() ); #TODO: makes sure that the layer's init method for the MODEL is invoked
332         me; #chainable
333 };
334
335 # This will read in the config and procedurally instantiate all requested layers and link them to toggle properties
336 # FIXME: this is currently GUI specific and doesn't yet support instrument use, i.e. needs to be generalized further
337 GenericMap.pickupFeatures = func(DIALOG_CANVAS) {
338         var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue();
339         # print("Picking up features for:", DIALOG_CANVAS.getPath() );
340         var layers=DIALOG_CANVAS.getNode("features").getChildren("layer");
341         foreach(var n; layers) {
342                 var name = n.getNode("name").getValue();
343                 var toggle = n.getNode("property").getValue();
344                 var init = n.getNode("init-property").getValue();
345                 init = dlgroot ~"/"~init;
346                 var property = dlgroot ~"/"~toggle;
347                 # print("Adding layer:",n.getNode("name").getValue() );
348                 me.setupFeature(name, property, init);
349         }
350         me; #chainable
351 }
352
353  # NOT a method, cmdarg() is no longer meaningful when the canvas nasal block is executed
354  # so this needs to be called in the dialog's OPEN block instead - TODO: generalize
355  #FIXME: move somewhere else, this really is a GUI helper  and should probably be generalized and moved to gui.nas
356 GenericMap.setupGUI = func (dialog, group) {
357         var group = globals.gui.findElementByName(cmdarg() , group);
358
359         var layers=dialog.getNode("features").getChildren("layer");
360         var template = dialog.getNode("checkbox-toggle-template");
361         var dlgroot =  dialog.getNode("features/dialog-root").getValue();
362         var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue();
363         var i=0;
364         foreach(var n; layers) {
365                 var name = n.getNode("name").getValue();
366                 var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue();
367                 var label  = n.getNode("description",1).getValue() or name;
368                 #var query_range = n.getNode("nav-query-range-property").getValue();
369                 #print("Query Range:", query_range);
370
371                 var default = n.getNode("default",1).getValue();
372                 default = (default=="enabled")?1:0;
373                 #print("Layer default for", name ," is:", default);
374                 setprop(toggle, default); # set the checkbox to its default setting
375
376                 var hide_checkbox = n.getNode("hide-checkbox",1).getValue();
377                 hide_checkbox = (hide_checkbox=="true")?1:0;
378
379                 var checkbox = group.getChild("checkbox",i, 1); #FIXME: compute proper offset dynamically, will currently overwrite other existing checkboxes!
380
381                 props.copy(template, checkbox);
382                 checkbox.getNode("name").setValue("display-"~name);
383                 checkbox.getNode("label").setValue(label);
384                 checkbox.getNode("property").setValue(toggle);
385                 checkbox.getNode("binding/object-name").setValue("display-"~name);
386                 checkbox.getNode("enabled",1).setValue(!hide_checkbox);
387                 i+=1;
388         }
389
390         #now add zoom buttons procedurally:
391         var template = dialog.getNode("zoom-template");
392         template.getNode("button[0]/binding[0]/property[0]").setValue(zoom);
393         template.getNode("text[0]/property[0]").setValue(zoom);
394         template.getNode("button[1]/binding[0]/property[0]").setValue(zoom);
395         template.getNode("button[1]/binding[0]/max[0]").setValue( i );
396         props.copy(template, group);
397 }
398
399
400 # this is currently "directly" invoked via a listener, needs to be changed
401 # to use the controller object instead
402 # TODO: adopt real MVC here
403 # FIXME: this must currently be explicitly called by the model, we need to use a wrapper to call it automatically instead!
404 LayerModel.notifyView  = func () {
405         # print("View notified");
406         me._view.update(); # update the layer/group
407
408         ### UGLY: disabled for now (probably breaks airport GUI dialog !)
409         ### me._map.updateState(); # update the map
410 }
411
412 # TODO: a "MapLayer" is a full MVC implementation that is owned by a "LayeredMap"
413
414 var MAP_LAYERS = {};
415 var register_layer = func(name, layer) MAP_LAYERS[name]=layer;
416
417 var MVC_FOLDER = getprop("/sim/fg-root") ~ "/Nasal/canvas/map/";
418 var load_modules = func(vec, ns='canvas')
419         foreach(var file; vec)
420                 io.load_nasal(MVC_FOLDER~file, ns); # TODO: should probably be using a different/sub-namespace!
421
422 # read in the file names dynamically: *.draw, *.model, *.layer
423 var files_with = func(ext) {
424         var results = [];
425         var all_files = directory(MVC_FOLDER);
426         foreach(var file; all_files) {
427                 if(substr(file, -size(ext)) != ext) continue;
428                 append(results, file);
429         }
430         return results;
431 }
432
433 setlistener("/nasal/canvas/loaded", func {
434         foreach(var ext; var extensions = ['.draw','.model','.layer'])
435                 load_modules(files_with(ext));
436
437         if (contains(canvas,"load_MapStructure"))
438                 load_MapStructure();
439
440         # canvas.MFD = {EFIS:}; # where we'll be storing all MFDs
441         # TODO: should be inside a separate subfolder, i.e. canvas/map/mfd
442         load_modules( files_with('.mfd'), 'canvas' );
443 });
444