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