Canvas Navigational Display:
- get rid of global variables and use instance variables - identified all important drawing routines and move them into *.draw files - changed to dynamic loading of *.draw *.model and *.layer files - implemented poor-man's controller hash to move use-case specific conditionals out of the draw files, and back into the instantiation, i.e. Gijs' EFIS class - started identifying stuff that is not specific to drawing, but to what is to be drawn, i.e. Model stuff - such as positioned queries, moved those out into *.model files - some more work on supporting more than a single ND MFD instance per aircraft - renamed a handful of SVG identifiers to avoid naming conflicts and to simplify usage of SVG IDs as member fields - moved all of the setlistener setup out of the fdm-initialized stub right into the ctor of the Efis class (actually that's controller stuff...) - initial MapStructure framework - aircraft-agnostic NavDisplay class - preparations for deprecating map.nas - additions to canvas.map - preparations for making NDStyles configurable via XML
This commit is contained in:
parent
315e22cad0
commit
a9576e8c8d
50 changed files with 2441 additions and 528 deletions
374
Nasal/canvas/MapStructure.nas
Normal file
374
Nasal/canvas/MapStructure.nas
Normal file
|
@ -0,0 +1,374 @@
|
||||||
|
var Symbol = {
|
||||||
|
# Static/singleton:
|
||||||
|
registry: {},
|
||||||
|
add: func(type, class)
|
||||||
|
me.registry[type] = class,
|
||||||
|
get: func(type)
|
||||||
|
if ((var class = me.registry[type]) == nil)
|
||||||
|
die("unknown type '"~type~"'");
|
||||||
|
else return class,
|
||||||
|
# Calls corresonding symbol constructor
|
||||||
|
# @param group #Canvas.Group to place this on.
|
||||||
|
new: func(type, group, arg...) {
|
||||||
|
var ret = call((var class = me.get(type)).new, [group]~arg, class);
|
||||||
|
ret.element.set("symbol-type", type);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
# Non-static:
|
||||||
|
df_controller: nil, # default controller
|
||||||
|
# Update the drawing of this object (position and others).
|
||||||
|
update: func()
|
||||||
|
die("update() not implemented for this symbol type!"),
|
||||||
|
draw: func(group, model, lod)
|
||||||
|
die("draw() not implemented for this symbol type!"),
|
||||||
|
del: func()
|
||||||
|
die("del() not implemented for this symbol type!"),
|
||||||
|
}; # of Symbol
|
||||||
|
|
||||||
|
Symbol.Controller = {
|
||||||
|
# Static/singleton:
|
||||||
|
registry: {},
|
||||||
|
add: func(type, class)
|
||||||
|
registry[type] = class,
|
||||||
|
get: func(type)
|
||||||
|
if ((var class = me.registry[type]) == nil)
|
||||||
|
die("unknown type '"~type~"'");
|
||||||
|
else return class,
|
||||||
|
# Calls corresonding symbol constructor
|
||||||
|
# @param model Model to place this on.
|
||||||
|
new: func(type, model, arg...)
|
||||||
|
return call((var class = me.get(type)).new, [model]~arg, class),
|
||||||
|
# Non-static:
|
||||||
|
# Update anything related to a particular model. Returns whether the object needs updating:
|
||||||
|
update: func(model) return 1,
|
||||||
|
# Initialize a controller to an object (or initialize the controller itself):
|
||||||
|
init: func(model) ,
|
||||||
|
# Delete an object from this controller (or delete the controller itself):
|
||||||
|
del: func(model) ,
|
||||||
|
# Return whether this symbol/object is visible:
|
||||||
|
isVisible: func(model) return 1,
|
||||||
|
# Get the position of this symbol/object:
|
||||||
|
getpos: func(model), # default provided below
|
||||||
|
}; # of Symbol.Controller
|
||||||
|
|
||||||
|
var getpos_fromghost = func(positioned_g)
|
||||||
|
return [positioned_g.lat, positioned_g.lon];
|
||||||
|
|
||||||
|
# Generic getpos: get lat/lon from any object:
|
||||||
|
# (geo.Coord and positioned ghost currently)
|
||||||
|
Symbol.Controller.getpos = func(obj) {
|
||||||
|
if (typeof(obj) == 'ghost')
|
||||||
|
if (ghosttype(obj) == 'positioned' or ghosttype(obj) == 'Navaid')
|
||||||
|
return getpos_fromghost(obj);
|
||||||
|
else
|
||||||
|
die("bad ghost of type '"~ghosttype(obj)~"'");
|
||||||
|
if (typeof(obj) == 'hash')
|
||||||
|
if (isa(obj, geo.Coord))
|
||||||
|
return obj.latlon();
|
||||||
|
die("no suitable getpos() found! Of type: "~typeof(obj));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var assert_m = func(hash, member)
|
||||||
|
if (!contains(hash, member))
|
||||||
|
die("required field not found: '"~member~"'");
|
||||||
|
var assert_ms = func(hash, members...)
|
||||||
|
foreach (var m; members)
|
||||||
|
if (m != nil) assert_m(hash, m);
|
||||||
|
|
||||||
|
|
||||||
|
var DotSym = {
|
||||||
|
parents: [Symbol],
|
||||||
|
element_id: nil,
|
||||||
|
# Static/singleton:
|
||||||
|
makeinstance: func(name, hash) {
|
||||||
|
assert_ms(hash,
|
||||||
|
"element_type", # type of Canvas element
|
||||||
|
#"element_id", # optional Canvas id
|
||||||
|
#"init", # initialize routine
|
||||||
|
"draw", # init/update routine
|
||||||
|
#getpos", # get position from model in [x_units,y_units] (optional)
|
||||||
|
);
|
||||||
|
hash.parents = [DotSym];
|
||||||
|
return Symbol.add(name, hash);
|
||||||
|
},
|
||||||
|
readinstance: func(file, name=nil) {
|
||||||
|
#print(file);
|
||||||
|
if (name == nil)
|
||||||
|
var name = split("/", file)[-1];
|
||||||
|
if (substr(name, size(name)-4) == ".draw")
|
||||||
|
name = substr(name, 0, size(name)-5);
|
||||||
|
var code = io.readfile(file);
|
||||||
|
var code = call(compile, [code], var err=[]);
|
||||||
|
if (size(err)) {
|
||||||
|
if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature
|
||||||
|
var e = split(" at line ", err[0]);
|
||||||
|
if (size(e) == 2)
|
||||||
|
err[0] = string.join("", [e[0], "\n at ", file, ", line ", e[1], "\n "]);
|
||||||
|
}
|
||||||
|
for (var i = 1; (var c = caller(i)) != nil; i += 1)
|
||||||
|
err ~= subvec(c, 2, 2);
|
||||||
|
debug.printerror(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
call(code, nil, nil, var hash = { parents:[DotSym] });
|
||||||
|
me.makeinstance(name, hash);
|
||||||
|
},
|
||||||
|
# For the instances returned from makeinstance:
|
||||||
|
# @param group The Canvas group to add this to.
|
||||||
|
# @param model A correct object (e.g. positioned ghost) as
|
||||||
|
# expected by the .draw file that represents
|
||||||
|
# metadata like position, speed, etc.
|
||||||
|
# @param controller Optional controller "glue". Each method
|
||||||
|
# is called with the model as the only argument.
|
||||||
|
new: func(group, model, controller=nil) {
|
||||||
|
var m = {
|
||||||
|
parents: [me],
|
||||||
|
group: group,
|
||||||
|
model: model,
|
||||||
|
controller: controller == nil ? me.df_controller : controller,
|
||||||
|
element: group.createChild(
|
||||||
|
me.element_type, me.element_id
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (m.controller != nil) {
|
||||||
|
#print("Initializing controller");
|
||||||
|
m.controller.init(model);
|
||||||
|
}
|
||||||
|
else die("default controller not found");
|
||||||
|
|
||||||
|
m.init();
|
||||||
|
return m;
|
||||||
|
},
|
||||||
|
del: func() {
|
||||||
|
#print("DotSym.del()");
|
||||||
|
me.deinit();
|
||||||
|
if (me.controller != nil)
|
||||||
|
me.controller.del(me.model);
|
||||||
|
call(func me.model.del(), nil, var err=[]); # try...
|
||||||
|
if (err[0] != "No such member: del") # ... and either catch or rethrow
|
||||||
|
die(err[0]);
|
||||||
|
me.element.del();
|
||||||
|
},
|
||||||
|
# Default wrappers:
|
||||||
|
init: func() me.draw(),
|
||||||
|
deinit: func(),
|
||||||
|
update: func() {
|
||||||
|
if (me.controller != nil) {
|
||||||
|
if (!me.controller.update(me.model)) return;
|
||||||
|
elsif (!me.controller.isVisible(me.model)) {
|
||||||
|
me.element.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
me.element.show();
|
||||||
|
me.draw();
|
||||||
|
var pos = me.controller.getpos(me.model);
|
||||||
|
if (size(pos) == 2)
|
||||||
|
pos~=[nil]; # fall through
|
||||||
|
if (size(pos) == 3)
|
||||||
|
var (lat,lon,rotation) = pos;
|
||||||
|
else die("bad position: "~debug.dump(pos));
|
||||||
|
me.element.setGeoPosition(lat,lon);
|
||||||
|
if (rotation != nil)
|
||||||
|
me.element.setRotation(rotation);
|
||||||
|
},
|
||||||
|
}; # of DotSym
|
||||||
|
|
||||||
|
# A layer that manages a list of symbols (using delta positioned handling).
|
||||||
|
var SymbolLayer = {
|
||||||
|
# Static/singleton:
|
||||||
|
registry: {},
|
||||||
|
add: func(type, class)
|
||||||
|
me.registry[type] = class,
|
||||||
|
get: func(type)
|
||||||
|
if ((var class = me.registry[type]) == nil)
|
||||||
|
die("unknown type '"~type~"'");
|
||||||
|
else return class,
|
||||||
|
# Non-static:
|
||||||
|
df_controller: nil, # default controller
|
||||||
|
df_priority: nil, # default priority for display sorting
|
||||||
|
type: nil, # type of #Symbol to add (MANDATORY)
|
||||||
|
id: nil, # id of the group #canvas.Element (OPTIONAL)
|
||||||
|
# @param group A group to place this on.
|
||||||
|
# @param controller A controller object (parents=[SymbolLayer.Controller])
|
||||||
|
# or implementation (parents[0].parents=[SymbolLayer.Controller]).
|
||||||
|
new: func(group, controller=nil) {
|
||||||
|
var m = {
|
||||||
|
parents: [me],
|
||||||
|
group: group.createChild("group", me.id), # TODO: the id is not properly set, but would be useful for debugging purposes (VOR, FIXES, NDB etc)
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
# FIXME: hack to expose type of layer:
|
||||||
|
if (caller(1)[1] == Map.addLayer) {
|
||||||
|
var this_type = caller(1)[0].type_arg;
|
||||||
|
if (this_type != nil)
|
||||||
|
m.group.set("symbol-layer-type", this_type);
|
||||||
|
}
|
||||||
|
if (controller == nil)
|
||||||
|
#controller = SymbolLayer.Controller.new(me.type, m);
|
||||||
|
controller = me.df_controller;
|
||||||
|
assert_m(controller, "parents");
|
||||||
|
if (controller.parents[0] == SymbolLayer.Controller)
|
||||||
|
controller = controller.new(m);
|
||||||
|
assert_m(controller, "parents");
|
||||||
|
assert_m(controller.parents[0], "parents");
|
||||||
|
if (controller.parents[0].parents[0] != SymbolLayer.Controller)
|
||||||
|
die("OOP error");
|
||||||
|
m.controller = controller;
|
||||||
|
m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m);
|
||||||
|
m.update();
|
||||||
|
return m;
|
||||||
|
},
|
||||||
|
update: func() {
|
||||||
|
me.searcher.update();
|
||||||
|
foreach (var e; me.list)
|
||||||
|
e.update();
|
||||||
|
},
|
||||||
|
del: func() {
|
||||||
|
#print("SymbolLayer.del()");
|
||||||
|
me.controller.del();
|
||||||
|
foreach (var e; me.list)
|
||||||
|
e.del();
|
||||||
|
},
|
||||||
|
findsym: func(positioned_g, del=0) {
|
||||||
|
forindex (var i; me.list) {
|
||||||
|
var e = me.list[i];
|
||||||
|
if (geo.PositionedSearch._equals(e.model, positioned_g)) {
|
||||||
|
if (del) {
|
||||||
|
# Remove this element from the list
|
||||||
|
var prev = subvec(me.list, 0, i);
|
||||||
|
var next = subvec(me.list, i+1);
|
||||||
|
me.list = prev~next;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
},
|
||||||
|
searchCmd: func() me.controller.searchCmd(),
|
||||||
|
# Adds a symbol.
|
||||||
|
onAdded: func(positioned_g)
|
||||||
|
append(me.list, Symbol.new(me.type, me.group, positioned_g)),
|
||||||
|
# Removes a symbol
|
||||||
|
onRemoved: func(positioned_g)
|
||||||
|
me.findsym(positioned_g, 1).del(),
|
||||||
|
}; # of SymbolLayer
|
||||||
|
|
||||||
|
# Class to manage controlling a #SymbolLayer.
|
||||||
|
# Currently handles:
|
||||||
|
# * Searching for new symbols (positioned ghosts or other objects with unique id's).
|
||||||
|
# * Updating the layer (e.g. on an update loop or on a property change).
|
||||||
|
SymbolLayer.Controller = {
|
||||||
|
# Static/singleton:
|
||||||
|
registry: {},
|
||||||
|
add: func(type, class)
|
||||||
|
me.registry[type] = class,
|
||||||
|
get: func(type)
|
||||||
|
if ((var class = me.registry[type]) == nil)
|
||||||
|
die("unknown type '"~type~"'");
|
||||||
|
else return class,
|
||||||
|
# Calls corresonding controller constructor
|
||||||
|
# @param layer The #SymbolLayer this controller is responsible for.
|
||||||
|
new: func(type, layer, arg...)
|
||||||
|
return call((var class = me.get(type)).new, [layer]~arg, class),
|
||||||
|
# Non-static:
|
||||||
|
run_update: func() {
|
||||||
|
me.layer.update();
|
||||||
|
},
|
||||||
|
# @return List of positioned objects.
|
||||||
|
searchCmd: func()
|
||||||
|
die("searchCmd() not implemented for this SymbolLayer.Controller type!"),
|
||||||
|
}; # of SymbolLayer.Controller
|
||||||
|
|
||||||
|
settimer(func {
|
||||||
|
Map.Controller = {
|
||||||
|
# Static/singleton:
|
||||||
|
registry: {},
|
||||||
|
add: func(type, class)
|
||||||
|
me.registry[type] = class,
|
||||||
|
get: func(type)
|
||||||
|
if ((var class = me.registry[type]) == nil)
|
||||||
|
die("unknown type '"~type~"'");
|
||||||
|
else return class,
|
||||||
|
# Calls corresonding controller constructor
|
||||||
|
# @param map The #SymbolMap this controller is responsible for.
|
||||||
|
new: func(type, layer, arg...)
|
||||||
|
return call((var class = me.get(type)).new, [map]~arg, class),
|
||||||
|
};
|
||||||
|
|
||||||
|
####### LOAD FILES #######
|
||||||
|
#print("loading files");
|
||||||
|
(func {
|
||||||
|
var FG_ROOT = getprop("/sim/fg-root");
|
||||||
|
var load = func(file, name) {
|
||||||
|
#print(file);
|
||||||
|
if (name == nil)
|
||||||
|
var name = split("/", file)[-1];
|
||||||
|
if (substr(name, size(name)-4) == ".draw")
|
||||||
|
name = substr(name, 0, size(name)-5);
|
||||||
|
#print("reading file");
|
||||||
|
var code = io.readfile(file);
|
||||||
|
#print("compiling file");
|
||||||
|
# This segfaults for some reason:
|
||||||
|
#var code = call(compile, [code], var err=[]);
|
||||||
|
var code = call(func compile(code, file), [code], var err=[]);
|
||||||
|
if (size(err)) {
|
||||||
|
#print("handling error");
|
||||||
|
if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature
|
||||||
|
var e = split(" at line ", err[0]);
|
||||||
|
if (size(e) == 2)
|
||||||
|
err[0] = string.join("", [e[0], "\n at ", file, ", line ", e[1], "\n "]);
|
||||||
|
}
|
||||||
|
for (var i = 1; (var c = caller(i)) != nil; i += 1)
|
||||||
|
err ~= subvec(c, 2, 2);
|
||||||
|
debug.printerror(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#print("calling code");
|
||||||
|
call(code, nil, nil, var hash = {});
|
||||||
|
#debug.dump(keys(hash));
|
||||||
|
return hash;
|
||||||
|
};
|
||||||
|
load(FG_ROOT~"/Nasal/canvas/map/VOR.lcontroller", "VOR");
|
||||||
|
DotSym.readinstance(FG_ROOT~"/Nasal/canvas/map/VOR.symbol", "VOR");
|
||||||
|
load(FG_ROOT~"/Nasal/canvas/map/VOR.scontroller", "VOR");
|
||||||
|
load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", "VOR");
|
||||||
|
})();
|
||||||
|
#print("finished loading files");
|
||||||
|
####### TEST SYMBOL #######
|
||||||
|
|
||||||
|
if (0)
|
||||||
|
settimer(func {
|
||||||
|
if (caller(0)[0] != globals.canvas)
|
||||||
|
return call(caller(0)[1], arg, nil, globals.canvas);
|
||||||
|
|
||||||
|
print("Running MapStructure test code");
|
||||||
|
var TestCanvas = canvas.new({
|
||||||
|
"name": "Map Test",
|
||||||
|
"size": [1024, 1024],
|
||||||
|
"view": [1024, 1024],
|
||||||
|
"mipmapping": 1
|
||||||
|
});
|
||||||
|
var dlg = canvas.Window.new([400, 400], "dialog");
|
||||||
|
dlg.setCanvas(TestCanvas);
|
||||||
|
var TestMap = TestCanvas.createGroup().createChild("map"); # we should not directly use a canvas here, but instead a LayeredMap.new()
|
||||||
|
TestMap.addLayer(factory: SymbolLayer, type_arg: "VOR"); # the ID should be also exposed in the property tree for each group (layer), i.e. better debugging
|
||||||
|
# Center the map's origin:
|
||||||
|
TestMap.setTranslation(512,512); # FIXME: don't hardcode these values, but read in canvas texture dimensions, otherwise it will break once someone uses non 1024x1024 textures ...
|
||||||
|
# Initialize a range (TODO: LayeredMap.Controller):
|
||||||
|
TestMap.set("range", 100);
|
||||||
|
# Little cursor of current position:
|
||||||
|
TestMap.createChild("path").rect(-5,-5,10,10).setColorFill(1,1,1).setColor(0,1,0);
|
||||||
|
# And make it move with our aircraft:
|
||||||
|
TestMap.setController("Aircraft position"); # from aircraftpos.controller
|
||||||
|
dlg.del = func() {
|
||||||
|
TestMap.del();
|
||||||
|
# call inherited 'del'
|
||||||
|
delete(me, "del");
|
||||||
|
me.del();
|
||||||
|
};
|
||||||
|
}, 1);
|
||||||
|
else print("MapStructure.nas: Testing code disabled, see $FG_ROOT/gui/dialogs/map-canvas.xml instead");
|
||||||
|
}, 0); # end ugly module init timer hack
|
||||||
|
|
|
@ -419,11 +419,90 @@ var Group = {
|
||||||
# which automatically get projected according to the specified projection.
|
# which automatically get projected according to the specified projection.
|
||||||
#
|
#
|
||||||
var Map = {
|
var Map = {
|
||||||
|
df_controller: nil,
|
||||||
new: func(ghost)
|
new: func(ghost)
|
||||||
{
|
{
|
||||||
return { parents: [Map, Group.new(ghost)] };
|
return { parents: [Map, Group.new(ghost)] };
|
||||||
}
|
},
|
||||||
# TODO
|
del: func()
|
||||||
|
{
|
||||||
|
#print("canvas.Map.del()");
|
||||||
|
call(func {
|
||||||
|
me.controller.del(me);
|
||||||
|
}, var err=[]);
|
||||||
|
if (size(err)) {
|
||||||
|
debug.printerror(err);
|
||||||
|
setsize(err, 0);
|
||||||
|
}
|
||||||
|
call(func {
|
||||||
|
foreach (var l; me.layers)
|
||||||
|
call(l[0].del, nil, l[0]);
|
||||||
|
setsize(me.layers, 0);
|
||||||
|
}, err);
|
||||||
|
if (size(err)) {
|
||||||
|
debug.printerror(err);
|
||||||
|
setsize(err, 0);
|
||||||
|
}
|
||||||
|
# call inherited 'del'
|
||||||
|
me.parents = subvec(me.parents,1);
|
||||||
|
me.del();
|
||||||
|
},
|
||||||
|
setController: func(controller=nil)
|
||||||
|
{
|
||||||
|
if (controller == nil)
|
||||||
|
controller = Map.df_controller;
|
||||||
|
elsif (typeof(controller) != 'hash')
|
||||||
|
controller = Map.Controller.get(controller);
|
||||||
|
if (controller.parents[0] != Map.Controller)
|
||||||
|
die("OOP error");
|
||||||
|
me.controller = controller.new(me);
|
||||||
|
|
||||||
|
return me;
|
||||||
|
},
|
||||||
|
addLayer: func(factory, type_arg=nil, priority=nil)
|
||||||
|
{
|
||||||
|
if (!contains(me, "layers"))
|
||||||
|
me.layers = [];
|
||||||
|
|
||||||
|
# Argument handling
|
||||||
|
if (type_arg != nil)
|
||||||
|
var type = factory.get(type_arg);
|
||||||
|
else var type = factory;
|
||||||
|
|
||||||
|
if (priority == nil)
|
||||||
|
priority = type.df_priority;
|
||||||
|
append(me.layers, [type.new(me), priority]);
|
||||||
|
if (priority != nil)
|
||||||
|
me._sort_priority();
|
||||||
|
return me;
|
||||||
|
},
|
||||||
|
setPos: func(lat,lon,hdg=nil)
|
||||||
|
{
|
||||||
|
me.set("ref-lat", lat);
|
||||||
|
me.set("ref-lon", lon);
|
||||||
|
if (hdg != nil)
|
||||||
|
me.set("hdg", hdg);
|
||||||
|
|
||||||
|
# me.map.set("range", 100);
|
||||||
|
},
|
||||||
|
# Update each layer on this Map. Called by
|
||||||
|
# me.controller.
|
||||||
|
update: func
|
||||||
|
{
|
||||||
|
foreach (var l; me.layers)
|
||||||
|
call(l[0].update, arg, l[0]);
|
||||||
|
return me;
|
||||||
|
},
|
||||||
|
# private:
|
||||||
|
_sort_priority: func()
|
||||||
|
{
|
||||||
|
me.layers = sort(me.layers, me._sort_cmp);
|
||||||
|
forindex (var i; me.layers)
|
||||||
|
me.layers[i].set("z-index", i);
|
||||||
|
},
|
||||||
|
_sort_cmp: func(a,b) {
|
||||||
|
a[1] != b[1] and a[1] != nil and b[1] != nil and (a[1] < b[1] ? -1 : 1)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
# Text
|
# Text
|
||||||
|
@ -442,7 +521,7 @@ var Text = {
|
||||||
},
|
},
|
||||||
# Set alignment
|
# Set alignment
|
||||||
#
|
#
|
||||||
# @param algin String, one of:
|
# @param align String, one of:
|
||||||
# left-top
|
# left-top
|
||||||
# left-center
|
# left-center
|
||||||
# left-bottom
|
# left-bottom
|
||||||
|
@ -653,8 +732,8 @@ var Path = {
|
||||||
cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg),
|
cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg),
|
||||||
cubic: func me.addSegment(me.VG_CUBIC_TO_REL, arg),
|
cubic: func me.addSegment(me.VG_CUBIC_TO_REL, arg),
|
||||||
# Add a smooth quadratic Bézier curve
|
# Add a smooth quadratic Bézier curve
|
||||||
quadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
|
squadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
|
||||||
quad: func me.addSegment(me.VG_SQUAD_TO_REL, arg),
|
squad: func me.addSegment(me.VG_SQUAD_TO_REL, arg),
|
||||||
# Add a smooth cubic Bézier curve
|
# Add a smooth cubic Bézier curve
|
||||||
scubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, arg),
|
scubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, arg),
|
||||||
scubic: func me.addSegment(me.VG_SCUBIC_TO_REL, arg),
|
scubic: func me.addSegment(me.VG_SCUBIC_TO_REL, arg),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
###
|
###
|
||||||
# map.nas - provide a high level method to create typical maps in FlightGear (airports, navaids, fixes and waypoints) for both, the GUI and instruments
|
# 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
|
# 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 (GUI checkboxes or cockpit hotspots)
|
# 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
|
# 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
|
# GOALS: have a single Nasal/Canvas wrapper for all sort of maps in FlightGear, that can be easily shared and reused for different purposes
|
||||||
|
@ -16,15 +16,17 @@
|
||||||
# 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
|
# 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:
|
# ROADMAP: Generalize this further, so that:
|
||||||
#
|
#
|
||||||
# - it can be easily reused
|
# - it can be easily reused
|
||||||
# - use a MVC approach, where layer-specific data is provided by a Model object
|
# - 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)
|
# - 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 instruments
|
# - 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 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
|
# - implement better GUI support (events) so that zooming/panning via mouse can be supported
|
||||||
# - make the whole thing styleable
|
# - 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
|
# - keep track of things getting added here and decide if they should better move to the core canvas module or the C++ code
|
||||||
#
|
#
|
||||||
|
@ -33,7 +35,7 @@
|
||||||
# - overload findNavaidsWithinRange() to support an optional position argument, so that arbitrary navaids can be looked up
|
# - 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)
|
# - add Nasal extension function to get scenery vector data (landclass)
|
||||||
# -
|
# -
|
||||||
# -
|
# -
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,28 +49,25 @@ var run_callbacks = func foreach(var c; callbacks) c();
|
||||||
var DEBUG=0;
|
var DEBUG=0;
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
var benchmark = debug.benchmark;
|
var benchmark = debug.benchmark;
|
||||||
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
var benchmark = func(label, code) code(); # NOP
|
var benchmark = func(label, code) code(); # NOP
|
||||||
}
|
}
|
||||||
|
|
||||||
var assert = func(label, expr) expr and die(label);
|
var assert = func(label, expr) expr and die(label);
|
||||||
|
|
||||||
# Mapping from surface codes to #TODO: make this XML-configurable
|
# Mapping from surface codes to colors (shared by runways.draw and taxiways.draw)
|
||||||
var SURFACECOLORS = {
|
var SURFACECOLORS = {
|
||||||
1 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
1 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
||||||
2 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
2 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
||||||
3 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
3 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
||||||
4 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
4 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
||||||
5 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
5 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
||||||
# Helipads
|
# Helipads
|
||||||
6 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
6 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
||||||
7 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
7 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
||||||
8 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
8 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
||||||
9 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
9 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
||||||
0 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
0 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,331 +76,197 @@ var SURFACECOLORS = {
|
||||||
var draw_layer = func(layer, callback, lod) {
|
var draw_layer = func(layer, callback, lod) {
|
||||||
var name= layer._view.get("id");
|
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
|
# 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
|
#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));
|
#print("Work items:", size(layer._model._elements));
|
||||||
benchmark("Drawing Layer:"~layer._view.get("id"), func
|
benchmark("Drawing Layer:"~layer._view.get("id"), func
|
||||||
foreach(var element; layer._model._elements) {
|
foreach(var element; layer._model._elements) {
|
||||||
#print(typeof(layer._view));
|
#print(typeof(layer._view));
|
||||||
#debug.dump(layer._view);
|
#debug.dump(layer._view);
|
||||||
callback(layer._view, element, lod); # ISSUE here
|
callback(layer._view, element, layer._controller, lod); # ISSUE here
|
||||||
});
|
});
|
||||||
if (! layer._model.hasData() ) print("Layer was EMPTY:", name);
|
if (! layer._model.hasData() ) print("Layer was EMPTY:", name);
|
||||||
if (DEBUG and name=="taxiways") fgcommand("profiler-stop");
|
#if (DEBUG and name=="taxiways") fgcommand("profiler-stop");
|
||||||
layer._drawn=1; #TODO: this should be encapsulated
|
layer._drawn=1; #TODO: this should be encapsulated
|
||||||
}
|
}
|
||||||
|
|
||||||
# Runway
|
# Runway
|
||||||
#
|
#
|
||||||
var Runway = {
|
var Runway = {
|
||||||
# Create Runway from hash
|
# Create Runway from hash
|
||||||
#
|
#
|
||||||
# @param rwy Hash containing runway data as returned from
|
# @param rwy Hash containing runway data as returned from
|
||||||
# airportinfo().runways[ <runway designator> ]
|
# airportinfo().runways[ <runway designator> ]
|
||||||
new: func(rwy)
|
new: func(rwy) {
|
||||||
{
|
return {
|
||||||
return {
|
parents: [Runway],
|
||||||
parents: [Runway],
|
rwy: rwy
|
||||||
rwy: rwy
|
};
|
||||||
};
|
},
|
||||||
},
|
# Get a point on the runway with the given offset
|
||||||
# Get a point on the runway with the given offset
|
#
|
||||||
#
|
# @param pos Position along the center line
|
||||||
# @param pos Position along the center line
|
# @param off Offset perpendicular to the center line
|
||||||
# @param off Offset perpendicular to the center line
|
pointOffCenterline: func(pos, off = 0) {
|
||||||
pointOffCenterline: func(pos, off = 0)
|
var coord = geo.Coord.new();
|
||||||
{
|
coord.set_latlon(me.rwy.lat, me.rwy.lon);
|
||||||
var coord = geo.Coord.new();
|
coord.apply_course_distance(me.rwy.heading, pos);
|
||||||
coord.set_latlon(me.rwy.lat, me.rwy.lon);
|
|
||||||
coord.apply_course_distance(me.rwy.heading, pos);
|
|
||||||
|
|
||||||
if( off )
|
if(off)
|
||||||
coord.apply_course_distance(me.rwy.heading + 90, off);
|
coord.apply_course_distance(me.rwy.heading + 90, off);
|
||||||
|
|
||||||
return ["N" ~ coord.lat(), "E" ~ coord.lon()];
|
return ["N" ~ coord.lat(), "E" ~ coord.lon()];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var make = func return {parents:arg};
|
var make = func return {parents:arg};
|
||||||
|
|
||||||
##
|
|
||||||
# TODO: Create a cache to reuse layers and layer data (i.e. runways)
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Todo: wrap parsesvg and return a function that memoizes the created canvas group, so that svg files only need to be parsed once
|
|
||||||
#
|
|
||||||
|
|
||||||
##
|
|
||||||
# TODO: Implement a real MVC design for "LayeredMaps" that have:
|
|
||||||
# - a "DataProvider" (i.e. Positioned objects)
|
|
||||||
# - a View (i.e. a Canvas)
|
|
||||||
# - a controller (i.e. input/output properties)
|
|
||||||
#
|
|
||||||
var MapModel = {}; # navaids, waypoints, fixes etc
|
|
||||||
MapModel.new = func make(MapModel);
|
|
||||||
|
|
||||||
var MapView = {}; # the canvas view, including a layer for each feature
|
|
||||||
MapView.new = func make(MapView);
|
|
||||||
|
|
||||||
var MapController = {}; # the property tree interface to manipulate the model/view via properties
|
|
||||||
MapController.new = func make(MapController);
|
|
||||||
|
|
||||||
var LazyView = {}; # Gets drawables on demand from the model - via property toggle
|
|
||||||
|
|
||||||
var DataProvider = {};
|
|
||||||
DataProvider.new = func make(DataProvider);
|
|
||||||
|
|
||||||
###
|
|
||||||
# for airports, navaids, fixes, waypoints etc
|
|
||||||
var PositionedProvider = {};
|
|
||||||
PositionedProvider.new = func make(DataProvider, PositionedProvider);
|
|
||||||
|
|
||||||
##
|
|
||||||
# Drawable
|
|
||||||
#
|
|
||||||
|
|
||||||
## LayerElement (UNUSED ATM):
|
|
||||||
# for runways, navaids, fixes, waypoints etc
|
|
||||||
# TODO: we should differentiate between "fairly static" vs. "dynamic" layers - i.e. navaids vs. traffic
|
|
||||||
var LayerElement = {_drawable:nil};
|
|
||||||
LayerElement.new = func(drawable) {
|
|
||||||
var temp = make(LayerElement);
|
|
||||||
temp._drawable=drawable;
|
|
||||||
return temp;
|
|
||||||
}
|
|
||||||
# a drawable is either a Nasal callback or a scalar, i.e. a path to an SVG file
|
|
||||||
LayerElement.draw = func(group) {
|
|
||||||
(typeof(me._drawable)=='func') and drawable(group) or canvas.parsesvg(group,_drawable);
|
|
||||||
}
|
|
||||||
|
|
||||||
# For static targets like Navaids, Fixes - i.e. geographic position doesn't change
|
|
||||||
var StaticLayerElement = {};
|
|
||||||
|
|
||||||
# For moving targets such as aircraft, multiplayer, ai traffic etc
|
|
||||||
var DynamicLayerElement = {};
|
|
||||||
|
|
||||||
var AnimatedLayerElement = {};
|
|
||||||
|
|
||||||
# for elements whose appearance may change depending on selected range (i.e. LOD)
|
|
||||||
var RangeAwareLayerElement = {};
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# A layer model is just a wrapper for a vector with elements
|
# A layer model is just a wrapper for a vector with elements
|
||||||
# either updated via a timer or via a listener
|
# either updated via a timer or via a listener (or both)
|
||||||
|
|
||||||
var LayerModel = {_elements:[], _view:, _controller: };
|
var LayerModel = {_elements:[], _view:, _controller:{query_range:func 100}, };
|
||||||
LayerModel.new = func make(LayerModel);
|
LayerModel.new = func make(LayerModel);
|
||||||
LayerModel.clear = func me._elements = [];
|
LayerModel.clear = func me._elements = [];
|
||||||
LayerModel.push = func (e) append(me._elements, e);
|
LayerModel.push = func (e) append(me._elements, e);
|
||||||
LayerModel.get = func me._elements;
|
LayerModel.get = func me._elements;
|
||||||
LayerModel.update = func;
|
LayerModel.update = func;
|
||||||
LayerModel.hasData = func size(me. _elements);
|
LayerModel.hasData = func size(me. _elements);
|
||||||
LayerModel.setView = func(v) me._view=v;
|
LayerModel.setView = func(v) me._view=v;
|
||||||
LayerModel.setController = func(c) me._controller=c;
|
LayerModel.setController = func(c) me._controller=c;
|
||||||
|
|
||||||
|
|
||||||
var LayerController = {};
|
|
||||||
LayerController.new = func make(LayerController);
|
|
||||||
|
|
||||||
##
|
|
||||||
# use timers to update the model/view (canvas)
|
|
||||||
var TimeBasedLayerController = {};
|
|
||||||
LayerController.new = func make(TimeBasedLayerController);
|
|
||||||
|
|
||||||
##
|
|
||||||
# use listeners to update the model/view (canvas)
|
|
||||||
#
|
|
||||||
var ListenerBasedLayerController = {};
|
|
||||||
ListenerBasedLayerController.new = func make(ListenerBasedLayerController);
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
# Uses, both, listeners and timers to update the model/view (canvas)
|
|
||||||
#
|
|
||||||
|
|
||||||
var HybridLayerController = {};
|
|
||||||
HybridLayerController.new = func make(HybridLayerController);
|
|
||||||
|
|
||||||
var ModelEvents = {INIT:, RESET:, UPDATE:};
|
|
||||||
var ViewEvents = {INIT:, RESET:, UPDATE:};
|
|
||||||
var ControllerEvents = {INIT:, RESET: , UPDATE:, ZOOM:, PAN:, };
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# A layer is mapped to a canvas group
|
# A layer is mapped to a canvas group
|
||||||
# Layers are linked to a single boolean property to toggle them on/off
|
# Layers are linked to a single boolean property to toggle them on/off
|
||||||
var Layer = { _model: ,
|
## FIXME: this is GUI specific ATM
|
||||||
_view: ,
|
var Layer = {
|
||||||
_controller: ,
|
_model: ,
|
||||||
_drawn:0,
|
_view: ,
|
||||||
};
|
_controller: ,
|
||||||
|
_drawn:0,
|
||||||
|
};
|
||||||
|
|
||||||
Layer.new = func(group, name, model) {
|
Layer.new = func(group, name, model, controller=nil) {
|
||||||
#print("Setting up new Layer:", name);
|
#print("Setting up new Layer:", name);
|
||||||
var m = make(Layer);
|
var m = make(Layer);
|
||||||
m._model = model.new();
|
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);
|
#print("Model name is:", m._model.name);
|
||||||
m._view = group.createChild("group",name);
|
m._view = group.createChild("group",name);
|
||||||
|
m._model._view = m;
|
||||||
m.name = name; #FIXME: not needed, there's already _view.get("id")
|
m.name = name; #FIXME: not needed, there's already _view.get("id")
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
Layer.hide = func me._view.setVisible(0);
|
Layer.hide = func me._view.setVisible(0);
|
||||||
Layer.show = func me._view.setVisible(1);
|
Layer.show = func me._view.setVisible(1);
|
||||||
#TODO: Unify toggle and update methods - and support lazy drawing (make it optional!)
|
#TODO: Unify toggle and update methods - and support lazy drawing (make it optional!)
|
||||||
Layer.toggle = func {
|
Layer.toggle = func {
|
||||||
# print("Toggling layer");
|
# print("Toggling layer");
|
||||||
var checkbox = getprop(me.display_layer);
|
var checkbox = getprop(me.display_layer);
|
||||||
if(checkbox and !me._drawn) {
|
if(checkbox and !me._drawn) {
|
||||||
# print("Lazy drawing");
|
# print("Lazy drawing");
|
||||||
me.draw();
|
me.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
#var state= me._view.getBool("visible");
|
#var state= me._view.getBool("visible");
|
||||||
#print("Toggle layer visibility ",me.display_layer," checkbox is", checkbox);
|
#print("Toggle layer visibility ",me.display_layer," checkbox is", checkbox);
|
||||||
#print("Layer id is:", me._view.get("id"));
|
#print("Layer id is:", me._view.get("id"));
|
||||||
#print("Drawn is:", me._drawn);
|
#print("Drawn is:", me._drawn);
|
||||||
checkbox?me._view.setVisible(1) : me._view.setVisible(0);
|
checkbox?me._view.setVisible(1) : me._view.setVisible(0);
|
||||||
}
|
}
|
||||||
Layer.reset = func {
|
Layer.reset = func {
|
||||||
me._view.removeAllChildren(); # clear the "real" canvas drawables
|
me._view.removeAllChildren(); # clear the "real" canvas drawables
|
||||||
me._model.clear(); # the vector is used for lazy rendering
|
me._model.clear(); # the vector is used for lazy rendering
|
||||||
assert("Model not emptied during layer reset!", me._model.hasData() );
|
assert("Model not emptied during layer reset!", me._model.hasData() );
|
||||||
me._drawn = 0;
|
me._drawn = 0;
|
||||||
}
|
}
|
||||||
#TODO: Unify toggle and update
|
#TODO: Unify toggle and update FIXME: GUI specific, not needed for 744 ND.nas
|
||||||
Layer.update = func {
|
Layer.update = func {
|
||||||
# print("Layer update: Check if layer is visible, if so, draw");
|
# print("Layer update: Check if layer is visible, if so, draw");
|
||||||
if (! getprop(me.display_layer)) return; # checkbox for layer not set
|
if (contains(me, "display_layer")) #UGLY HACK
|
||||||
if (!me._model.hasData() ) return; # no data available
|
if (! getprop(me.display_layer)) return; # checkbox for layer not set
|
||||||
# print("Trying to draw");
|
|
||||||
me.draw();
|
if (!me._model.hasData() ) return; # no data available
|
||||||
|
# print("Trying to draw");
|
||||||
|
me.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
Layer.setDraw = func(callback) me.draw = callback;
|
Layer.setDraw = func(callback) me.draw = callback;
|
||||||
Layer.setController = func(c) me._controller=c; # TODO: implement
|
Layer.setController = func(c) me._controller=c; # TODO: implement
|
||||||
Layer.setModel = func(m) nil; # TODO: implement
|
Layer.setModel = func(m) nil; # TODO: implement
|
||||||
|
|
||||||
|
|
||||||
##TODO: differentiate between layers with a single object (i.e. aircraft) and multiple objects (airports)
|
|
||||||
|
|
||||||
##
|
|
||||||
# We may need to display some stuff that isn't strictly a geopgraphic feature, but just a chart feature
|
|
||||||
#
|
|
||||||
var CartographicLayer = {};
|
|
||||||
|
|
||||||
#TODO:
|
|
||||||
var InteractiveLayer = {};
|
|
||||||
|
|
||||||
###
|
|
||||||
# PositionedLayer
|
|
||||||
#
|
|
||||||
# layer of positioned objects (i.e. have lat,lon,alt)
|
|
||||||
#
|
|
||||||
var PositionedLayer = {};
|
|
||||||
PositionedLayer.new = func() {
|
|
||||||
make( Layer.new() , PositionedLayer );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
###
|
|
||||||
# CachedLayer
|
|
||||||
#
|
|
||||||
# when re-centering on an airport already loaded, we don't want to reload it
|
|
||||||
# but change the reference point and load missing airports
|
|
||||||
|
|
||||||
var CachedLayer = {};
|
|
||||||
|
|
||||||
##
|
|
||||||
#
|
|
||||||
var AirportProvider = {};
|
|
||||||
AirportProvider.new = func make(AirportProvider);
|
|
||||||
AirportProvider.get = func {
|
|
||||||
return airportinfo("ksfo");
|
|
||||||
}
|
|
||||||
|
|
||||||
### Data Providers (preparation for MVC version):
|
|
||||||
# TODO: should use the LayerModel class
|
|
||||||
#
|
|
||||||
|
|
||||||
##
|
|
||||||
# Manage a bunch of layers
|
|
||||||
#
|
|
||||||
|
|
||||||
var LayerManager = {};
|
|
||||||
|
|
||||||
# WXR ?
|
|
||||||
|
|
||||||
# TODO: Stub
|
|
||||||
var MapBehavior = {};
|
|
||||||
MapBehavior.new = make(MapBehavior);
|
|
||||||
MapBehavior.zoom = func;
|
|
||||||
MapBehavior.center = func;
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# A layered map consists of several layers
|
# A layered map consists of several layers
|
||||||
# TODO: Support nested LayeredMaps, where a LayeredMap may contain other LayeredMaps
|
# 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
|
# TODO: use MapBehavior here and move the zoom/refpos methods there, so that map behavior can be easily customized
|
||||||
var LayeredMap = { ranges:[],
|
var LayeredMap = {
|
||||||
zoom_property:nil, listeners:[],
|
ranges:[],
|
||||||
update_property:nil, layers:[],
|
zoom_property:nil, listeners:[],
|
||||||
};
|
update_property:nil, layers:[],
|
||||||
LayeredMap.new = func(parent, name)
|
};
|
||||||
return make(LayeredMap, parent.createChild("map",name) );
|
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
|
LayeredMap.listen = func(p,c) { #FIXME: listening should be managed by each m/v/c separately
|
||||||
# print("Setting up LayeredMap-managed listener:", p);
|
# print("Setting up LayeredMap-managed listener:", p);
|
||||||
append(me.listeners, setlistener(p, c));
|
append(me.listeners, setlistener(p, c));
|
||||||
}
|
}
|
||||||
|
|
||||||
LayeredMap.initializeLayers = func {
|
LayeredMap.initializeLayers = func {
|
||||||
# print("initializing all layers and updating");
|
# print("initializing all layers and updating");
|
||||||
foreach(var l; me.layers)
|
foreach(var l; me.layers)
|
||||||
l.update();
|
l.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
LayeredMap.setRefPos = func(lat, lon) {
|
LayeredMap.setRefPos = func(lat, lon) {
|
||||||
# print("RefPos set");
|
# print("RefPos set");
|
||||||
me._node.getNode("ref-lat", 1).setDoubleValue(lat);
|
me._node.getNode("ref-lat", 1).setDoubleValue(lat);
|
||||||
me._node.getNode("ref-lon", 1).setDoubleValue(lon);
|
me._node.getNode("ref-lon", 1).setDoubleValue(lon);
|
||||||
me; # chainable
|
|
||||||
}
|
|
||||||
LayeredMap.setHdg = func(hdg) {
|
|
||||||
me._node.getNode("hdg",1).setDoubleValue(hdg);
|
|
||||||
me; # chainable
|
me; # chainable
|
||||||
}
|
}
|
||||||
|
LayeredMap.setHdg = func(hdg) {
|
||||||
|
me._node.getNode("hdg",1).setDoubleValue(hdg);
|
||||||
|
me; # chainable
|
||||||
|
}
|
||||||
|
|
||||||
LayeredMap.updateZoom = func {
|
LayeredMap.updateZoom = func {
|
||||||
var z = me.zoom_property.getValue() or 0;
|
var z = me.zoom_property.getValue() or 0;
|
||||||
z = math.max(0, math.min(z, size(me.ranges) - 1));
|
z = math.max(0, math.min(z, size(me.ranges) - 1));
|
||||||
me.zoom_property.setIntValue(z);
|
me.zoom_property.setIntValue(z);
|
||||||
var zoom = me.ranges[size(me.ranges) - 1 - z];
|
var zoom = me.ranges[size(me.ranges) - 1 - z];
|
||||||
# print("Setting zoom range to:", zoom);
|
# print("Setting zoom range to:", zoom);
|
||||||
benchmark("Zooming map:"~zoom, func
|
benchmark("Zooming map:"~zoom, func {
|
||||||
{
|
|
||||||
me._node.getNode("range", 1).setDoubleValue(zoom);
|
me._node.getNode("range", 1).setDoubleValue(zoom);
|
||||||
# TODO update center/limit translation to keep airport always visible
|
# TODO update center/limit translation to keep airport always visible
|
||||||
});
|
});
|
||||||
me; #chainable
|
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
|
# 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 {
|
LayeredMap.updateState = func {
|
||||||
# center map on airport TODO: should be moved to a method and wrapped with a controller so that behavior can be customizeda
|
# 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];
|
#var apt = me.layers[0]._model._elements[0];
|
||||||
# FIXME:
|
# FIXME:
|
||||||
#me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon);
|
#me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon);
|
||||||
|
|
||||||
me.setHdg(0.0);
|
me.setHdg(0.0);
|
||||||
me.updateZoom();
|
me.updateZoom();
|
||||||
}
|
}
|
||||||
|
|
||||||
#
|
#
|
||||||
# TODO: this is currently GUI specific and not re-usable for instruments
|
# TODO: this is currently GUI specific and not re-usable for instruments
|
||||||
LayeredMap.setupZoom = func(dialog) {
|
LayeredMap.setupZoom = func(dialog) {
|
||||||
var dlgroot = dialog.getNode("features/dialog-root").getValue();#FIXME: GUI specific - needs to be re-implemented for instruments
|
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 !!!
|
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");
|
ranges=dialog.getNode("features/ranges").getChildren("range");
|
||||||
if( size(me.ranges) == 0 )
|
if( size(me.ranges) == 0 )
|
||||||
|
@ -413,132 +278,123 @@ LayeredMap.updateState = func {
|
||||||
me.listen(me.zoom_property, func me.updateZoom() );
|
me.listen(me.zoom_property, func me.updateZoom() );
|
||||||
me.updateZoom();
|
me.updateZoom();
|
||||||
me; #chainable
|
me; #chainable
|
||||||
}
|
}
|
||||||
LayeredMap.setZoom = func {} #TODO
|
LayeredMap.setZoom = func {} #TODO
|
||||||
|
|
||||||
LayeredMap.resetLayers = func {
|
|
||||||
|
|
||||||
benchmark("Resetting LayeredMap", func
|
|
||||||
foreach(var l; me.layers) { #TODO: hide all layers, hide map
|
|
||||||
l.reset();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
|
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!
|
#FIXME: listener management should be done at the MVC level, for each component - not as part of the LayeredMap!
|
||||||
LayeredMap.cleanup_listeners = func {
|
LayeredMap.cleanup_listeners = func {
|
||||||
# print("Cleaning up listeners");
|
# print("Cleaning up listeners");
|
||||||
foreach(var l; me.listeners)
|
foreach(var l; me.listeners)
|
||||||
removelistener(l);
|
removelistener(l);
|
||||||
# TODO check why me.listeners = []; doesn't work. Maybe this is a Nasal bug
|
# TODO check why me.listeners = []; doesn't work. Maybe this is a Nasal bug
|
||||||
# and the old vector is somehow used again.
|
# and the old vector is somehow used again.
|
||||||
setsize(me.listeners, 0);
|
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
|
# 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
|
# 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
|
# 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
|
# TODO: generalize the XML-parametrization and move it to a helper class
|
||||||
|
|
||||||
var GenericMap = { };
|
var GenericMap = { };
|
||||||
GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap);
|
GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap);
|
||||||
|
|
||||||
GenericMap.setupLayer = func(layer, property) {
|
GenericMap.setupLayer = func(layer, property) {
|
||||||
var l = MAP_LAYERS[layer].new(me, layer); # Layer.new(me, layer);
|
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
|
l.display_layer = property; #FIXME: use controller object instead here and this overlaps with update_property
|
||||||
#print("Set up layer with toggle property=", property);
|
#print("Set up layer with toggle property=", property);
|
||||||
l._view.setVisible( getprop(property) ) ;
|
l._view.setVisible( getprop(property) ) ;
|
||||||
append(me.layers, l);
|
append(me.layers, l);
|
||||||
return l;
|
return l;
|
||||||
}
|
}
|
||||||
|
|
||||||
# features are layers - so this will do layer setup and then register listeners for each layer
|
# features are layers - so this will do layer setup and then register listeners for each layer
|
||||||
GenericMap.setupFeature = func(layer, property, init ) {
|
GenericMap.setupFeature = func(layer, property, init ) {
|
||||||
var l=me.setupLayer( layer, property );
|
var l=me.setupLayer( layer, property );
|
||||||
me.listen(property, func l.toggle() ); #TODO: should use the controller object here !
|
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._update_property=property; #TODO: move somewhere else - this is the property that is mapped to the CHECKBOX
|
||||||
l._model._view_handle = l; #FIXME: very crude, set a handle to the view(group), so that the model can notify it (for updates)
|
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_handle = me; #FIXME: added here so that layers can send update requests to the parent map
|
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);
|
#print("Setting up layer init for property:", init);
|
||||||
|
|
||||||
l._model._input_property = init; # FIXME: init property = input property - needs to be improved!
|
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.listen(init, func l._model.init() ); #TODO: makes sure that the layer's init method for the MODEL is invoked
|
||||||
me; #chainable
|
me; #chainable
|
||||||
};
|
};
|
||||||
|
|
||||||
# This will read in the config and procedurally instantiate all requested layers and link them to toggle properties
|
# 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
|
# 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) {
|
GenericMap.pickupFeatures = func(DIALOG_CANVAS) {
|
||||||
var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue();
|
var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue();
|
||||||
# print("Picking up features for:", DIALOG_CANVAS.getPath() );
|
# print("Picking up features for:", DIALOG_CANVAS.getPath() );
|
||||||
var layers=DIALOG_CANVAS.getNode("features").getChildren("layer");
|
var layers=DIALOG_CANVAS.getNode("features").getChildren("layer");
|
||||||
foreach(var n; layers) {
|
foreach(var n; layers) {
|
||||||
var name = n.getNode("name").getValue();
|
var name = n.getNode("name").getValue();
|
||||||
var toggle = n.getNode("property").getValue();
|
var toggle = n.getNode("property").getValue();
|
||||||
var init = n.getNode("init-property").getValue();
|
var init = n.getNode("init-property").getValue();
|
||||||
init = dlgroot ~"/"~init;
|
init = dlgroot ~"/"~init;
|
||||||
var property = dlgroot ~"/"~toggle;
|
var property = dlgroot ~"/"~toggle;
|
||||||
# print("Adding layer:",n.getNode("name").getValue() );
|
# print("Adding layer:",n.getNode("name").getValue() );
|
||||||
me.setupFeature(name, property, init);
|
me.setupFeature(name, property, init);
|
||||||
}
|
}
|
||||||
me;
|
me; #chainable
|
||||||
}
|
}
|
||||||
|
|
||||||
# NOT a method, cmdarg() is no longer meaningful when the canvas nasal block is executed
|
# 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
|
# so this needs to be called in the dialog's OPEN block instead - TODO: generalize
|
||||||
#FIXME: move somewhere else, this is a GUI helper and should probably be generalized and moved to gui.nas
|
#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) {
|
GenericMap.setupGUI = func (dialog, group) {
|
||||||
var group = globals.gui.findElementByName(cmdarg() , group);
|
var group = globals.gui.findElementByName(cmdarg() , group);
|
||||||
|
|
||||||
var layers=dialog.getNode("features").getChildren("layer");
|
var layers=dialog.getNode("features").getChildren("layer");
|
||||||
var template = dialog.getNode("checkbox-toggle-template");
|
var template = dialog.getNode("checkbox-toggle-template");
|
||||||
var dlgroot = dialog.getNode("features/dialog-root").getValue();
|
var dlgroot = dialog.getNode("features/dialog-root").getValue();
|
||||||
var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue();
|
var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue();
|
||||||
var i=0;
|
var i=0;
|
||||||
foreach(var n; layers) {
|
foreach(var n; layers) {
|
||||||
var name = n.getNode("name").getValue();
|
var name = n.getNode("name").getValue();
|
||||||
var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue();
|
var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue();
|
||||||
var label = n.getNode("description",1).getValue() or name;
|
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();
|
var default = n.getNode("default",1).getValue();
|
||||||
default = (default=="enabled")?1:0;
|
default = (default=="enabled")?1:0;
|
||||||
#print("Layer default for", name ," is:", default);
|
#print("Layer default for", name ," is:", default);
|
||||||
setprop(toggle, default); # set the checkbox to its default setting
|
setprop(toggle, default); # set the checkbox to its default setting
|
||||||
|
|
||||||
var hide_checkbox = n.getNode("hide-checkbox",1).getValue();
|
var hide_checkbox = n.getNode("hide-checkbox",1).getValue();
|
||||||
hide_checkbox = (hide_checkbox=="true")?1:0;
|
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!
|
var checkbox = group.getChild("checkbox",i, 1); #FIXME: compute proper offset dynamically, will currently overwrite other existing checkboxes!
|
||||||
|
|
||||||
props.copy(template, checkbox);
|
props.copy(template, checkbox);
|
||||||
checkbox.getNode("name").setValue("display-"~name);
|
checkbox.getNode("name").setValue("display-"~name);
|
||||||
checkbox.getNode("label").setValue(label);
|
checkbox.getNode("label").setValue(label);
|
||||||
checkbox.getNode("property").setValue(toggle);
|
checkbox.getNode("property").setValue(toggle);
|
||||||
checkbox.getNode("binding/object-name").setValue("display-"~name);
|
checkbox.getNode("binding/object-name").setValue("display-"~name);
|
||||||
checkbox.getNode("enabled",1).setValue(!hide_checkbox);
|
checkbox.getNode("enabled",1).setValue(!hide_checkbox);
|
||||||
i+=1;
|
i+=1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#add zoom buttons procedurally:
|
#now add zoom buttons procedurally:
|
||||||
var template = dialog.getNode("zoom-template");
|
var template = dialog.getNode("zoom-template");
|
||||||
template.getNode("button[0]/binding[0]/property[0]").setValue(zoom);
|
template.getNode("button[0]/binding[0]/property[0]").setValue(zoom);
|
||||||
template.getNode("text[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]/property[0]").setValue(zoom);
|
||||||
template.getNode("button[1]/binding[0]/max[0]").setValue( i );
|
template.getNode("button[1]/binding[0]/max[0]").setValue( i );
|
||||||
props.copy(template, group);
|
props.copy(template, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
###
|
|
||||||
# TODO: StylableGenericMap (colors, fonts, symbols)
|
|
||||||
#
|
|
||||||
|
|
||||||
var AirportMap = {};
|
|
||||||
AirportMap.new = func(parent,name) make(GenericMap.new(parent,name), AirportMap);
|
|
||||||
#TODO: Use real MVC (DataProvider/PositionedProvider) here
|
|
||||||
|
|
||||||
|
|
||||||
# this is currently "directly" invoked via a listener, needs to be changed
|
# this is currently "directly" invoked via a listener, needs to be changed
|
||||||
|
@ -546,48 +402,37 @@ var AirportMap = {};
|
||||||
# TODO: adopt real MVC here
|
# 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!
|
# FIXME: this must currently be explicitly called by the model, we need to use a wrapper to call it automatically instead!
|
||||||
LayerModel.notifyView = func () {
|
LayerModel.notifyView = func () {
|
||||||
# print("View notified");
|
# print("View notified");
|
||||||
me._view_handle.update(); # update the layer/group
|
me._view.update(); # update the layer/group
|
||||||
me._map_handle.updateState(); # update the map
|
|
||||||
|
### UGLY: disabled for now (probably breaks airport GUI dialog !)
|
||||||
|
### me._map.updateState(); # update the map
|
||||||
}
|
}
|
||||||
|
|
||||||
# ID
|
|
||||||
var SingleAirportProvider = {};
|
|
||||||
|
|
||||||
# inputs: position, range
|
|
||||||
var MultiAirportProvider = {};
|
|
||||||
|
|
||||||
#TODO: remove and unify with update()
|
|
||||||
AirportMap.init = func {
|
|
||||||
me.resetLayers();
|
|
||||||
me.updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
# MultiObjectLayer:
|
|
||||||
# - Airports
|
|
||||||
# - Traffic (MP/AI)
|
|
||||||
# - Navaids
|
|
||||||
#
|
|
||||||
|
|
||||||
# TODO: a "MapLayer" is a full MVC implementation that is owned by a "LayeredMap"
|
# TODO: a "MapLayer" is a full MVC implementation that is owned by a "LayeredMap"
|
||||||
|
|
||||||
var MAP_LAYERS = {};
|
var MAP_LAYERS = {};
|
||||||
var register_layer = func(name, layer) MAP_LAYERS[name]=layer;
|
var register_layer = func(name, layer) MAP_LAYERS[name]=layer;
|
||||||
|
|
||||||
var MVC_FOLDER = getprop("/sim/fg-root") ~ "/Nasal/canvas/map/";
|
var MVC_FOLDER = getprop("/sim/fg-root") ~ "/Nasal/canvas/map/";
|
||||||
var load_modules = func(vec) foreach(var file; vec) io.load_nasal(MVC_FOLDER~file, "canvas");
|
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!
|
||||||
|
|
||||||
# TODO: read in the file names dynamically: *.draw, *.model, *.layer
|
# 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;
|
||||||
|
}
|
||||||
|
foreach(var ext; var extensions = ['.draw','.model','.layer'])
|
||||||
|
load_modules(files_with(ext));
|
||||||
|
|
||||||
var DRAWABLES = ["navaid.draw", "parking.draw", "runways.draw", "taxiways.draw", "tower.draw"];
|
|
||||||
load_modules(DRAWABLES);
|
|
||||||
|
|
||||||
var MODELS = ["airports.model", "navaids.model",];
|
# canvas.MFD = {EFIS:}; # where we'll be storing all MFDs
|
||||||
load_modules(MODELS);
|
# TODO: should be inside a separate subfolder, i.e. canvas/map/mfd
|
||||||
|
load_modules( files_with('.mfd'), 'canvas' );
|
||||||
var LAYERS = ["runways.layer", "taxiways.layer", "parking.layer", "tower.layer", "navaids.layer","test.layer",];
|
|
||||||
load_modules(LAYERS);
|
|
||||||
|
|
||||||
#TODO: Implement!
|
|
||||||
var CONTROLLERS = [];
|
|
||||||
load_modules(CONTROLLERS);
|
|
||||||
|
|
52
Nasal/canvas/map/VOR.lcontroller
Normal file
52
Nasal/canvas/map/VOR.lcontroller
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Class things:
|
||||||
|
var parents = [SymbolLayer.Controller];
|
||||||
|
var __self__ = caller(0)[0];
|
||||||
|
SymbolLayer.Controller.add("VOR", __self__);
|
||||||
|
SymbolLayer.add("VOR", {
|
||||||
|
parents: [SymbolLayer],
|
||||||
|
type: "VOR", # Symbol type
|
||||||
|
df_controller: __self__, # controller to use by default -- this one
|
||||||
|
});
|
||||||
|
var new = func(layer) {
|
||||||
|
var m = {
|
||||||
|
parents: [__self__],
|
||||||
|
layer: layer,
|
||||||
|
active_vors: [],
|
||||||
|
navNs: props.globals.getNode("instrumentation").getChildren("nav"),
|
||||||
|
listeners: [],
|
||||||
|
};
|
||||||
|
setsize(m.active_vors, size(m.navNs));
|
||||||
|
foreach (var navN; m.navNs) {
|
||||||
|
append(m.listeners, setlistener(
|
||||||
|
navN.getNode("frequencies/selected-mhz"),
|
||||||
|
func m.changed_freq()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
#call(debug.dump, keys(layer));
|
||||||
|
m.changed_freq(update:0);
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
var del = func() {
|
||||||
|
#print("VOR.lcontroller.del()");
|
||||||
|
foreach (var l; me.listeners)
|
||||||
|
removelistener(l);
|
||||||
|
};
|
||||||
|
|
||||||
|
# Controller methods
|
||||||
|
var isActive = func(model) {
|
||||||
|
var my_freq = model.frequency/100;
|
||||||
|
foreach (var freq; me.active_vors)
|
||||||
|
if (freq == my_freq) return 1;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
var changed_freq = func(update=1) {
|
||||||
|
#debug.dump(me.active_vors);
|
||||||
|
foreach (var navN; me.navNs)
|
||||||
|
me.active_vors[ navN.getIndex() ] = navN.getValue("frequencies/selected-mhz");
|
||||||
|
if (update) me.layer.update();
|
||||||
|
};
|
||||||
|
var searchCmd = func {
|
||||||
|
#print("Run query");
|
||||||
|
return positioned.findWithinRange(100, 'vor'); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch
|
||||||
|
};
|
||||||
|
|
10
Nasal/canvas/map/VOR.scontroller
Normal file
10
Nasal/canvas/map/VOR.scontroller
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Class things:
|
||||||
|
var parents = [Symbol.Controller];
|
||||||
|
var __self__ = caller(0)[0];
|
||||||
|
Symbol.registry["VOR"].df_controller = __self__;
|
||||||
|
var new = func(model) ; # this controller doesn't need an instance
|
||||||
|
var LayerController = SymbolLayer.registry["VOR"];
|
||||||
|
var isActive = func(model) LayerController.isActive(model);
|
||||||
|
var query_range = func()
|
||||||
|
die("VOR.scontroller.query_range /MUST/ be provided by implementation");
|
||||||
|
|
52
Nasal/canvas/map/VOR.symbol
Normal file
52
Nasal/canvas/map/VOR.symbol
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Read by the DotSym.readinstance; each variable becomes a derived class's member/method
|
||||||
|
|
||||||
|
var element_type = "group"; # we want a group, becomes "me.element"
|
||||||
|
var inited = 0; # this allows us to track whether draw() is an init() or an update()
|
||||||
|
var range_vor = nil; # two elements that get drawn when needed
|
||||||
|
var radial_vor = nil; # if one is nil, the other has to be nil
|
||||||
|
|
||||||
|
var draw = func {
|
||||||
|
if (me.inited) {
|
||||||
|
# Update
|
||||||
|
if (me.controller.isActive(me.model)) {
|
||||||
|
if (me.range_vor == nil) {
|
||||||
|
var rangeNm = me.controller.query_range();
|
||||||
|
# print("VOR is tuned:", me.model.id);
|
||||||
|
var radius = (me.model.range_nm/rangeNm)*345;
|
||||||
|
me.range_vor = me.element.createChild("path")
|
||||||
|
.moveTo(-radius,0)
|
||||||
|
.arcSmallCW(radius,radius,0,2*radius,0)
|
||||||
|
.arcSmallCW(radius,radius,0,-2*radius,0)
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setStrokeDashArray([5, 15, 5, 15, 5])
|
||||||
|
.setColor(0,1,0);
|
||||||
|
|
||||||
|
var course = controller.get_tuned_course(me.model.frequency/100);
|
||||||
|
vor_grp.createChild("path")
|
||||||
|
.moveTo(0,-radius)
|
||||||
|
.vert(2*radius)
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setStrokeDashArray([15, 5, 15, 5, 15])
|
||||||
|
.setColor(0,1,0)
|
||||||
|
.setRotation(course*D2R);
|
||||||
|
icon_vor.setColor(0,1,0);
|
||||||
|
}
|
||||||
|
me.range_vor.show();
|
||||||
|
me.radial_vor.show();
|
||||||
|
} else {
|
||||||
|
me.range_vor.hide();
|
||||||
|
me.radial_vor.hide();
|
||||||
|
}
|
||||||
|
} else # Init
|
||||||
|
me.element.createChild("path")
|
||||||
|
.moveTo(-15,0)
|
||||||
|
.lineTo(-7.5,12.5)
|
||||||
|
.lineTo(7.5,12.5)
|
||||||
|
.lineTo(15,0)
|
||||||
|
.lineTo(7.5,-12.5)
|
||||||
|
.lineTo(-7.5,-12.5)
|
||||||
|
.close()
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setColor(0,0.6,0.85);
|
||||||
|
};
|
||||||
|
|
41
Nasal/canvas/map/aircraftpos.controller
Normal file
41
Nasal/canvas/map/aircraftpos.controller
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Class things:
|
||||||
|
var parents = [Map.Controller];
|
||||||
|
var __self__ = caller(0)[0];
|
||||||
|
Map.Controller.add("Aircraft position", __self__);
|
||||||
|
Map.df_controller = __self__;
|
||||||
|
var new = func(map) {
|
||||||
|
var m = {
|
||||||
|
parents: [__self__],
|
||||||
|
map: map,
|
||||||
|
_pos: nil, _time: nil,
|
||||||
|
};
|
||||||
|
m.timer = maketimer(0, m, update_pos);
|
||||||
|
m.timer.start();
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
var del = func(map) {
|
||||||
|
if (map != me.map) die();
|
||||||
|
me.timer.stop();
|
||||||
|
};
|
||||||
|
|
||||||
|
# Controller methods
|
||||||
|
var update_pos = func {
|
||||||
|
var (lat,lon) = (var pos = geo.aircraft_position()).latlon();
|
||||||
|
var time = systime();
|
||||||
|
me.map.setPos(lat, lon, getprop("/orientation/heading-deg"));
|
||||||
|
if (me._pos == nil)
|
||||||
|
me._pos = geo.Coord.new(pos);
|
||||||
|
else {
|
||||||
|
var dist = me._pos.direct_distance_to(pos);
|
||||||
|
# 2 NM until we update again
|
||||||
|
if (dist < 2 * NM2M) return;
|
||||||
|
# Update at most every 4 seconds to avoid escessive stutter:
|
||||||
|
elsif (time - me._time < 4) return;
|
||||||
|
}
|
||||||
|
#print("update aircraft position");
|
||||||
|
var (x,y,z) = pos.xyz();
|
||||||
|
me._pos.set_xyz(x,y,z);
|
||||||
|
me._time = time;
|
||||||
|
me.map.update();
|
||||||
|
};
|
||||||
|
|
33
Nasal/canvas/map/airport-nd.draw
Normal file
33
Nasal/canvas/map/airport-nd.draw
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
##
|
||||||
|
# draws a single airport (ND style)
|
||||||
|
#
|
||||||
|
var draw_apt = func (group, apt, controller=nil, lod=0) {
|
||||||
|
var lat = apt.lat;
|
||||||
|
var lon = apt.lon;
|
||||||
|
var name = apt.id;
|
||||||
|
# print("drawing nd airport:", name);
|
||||||
|
|
||||||
|
var apt_grp = group.createChild("group", name);
|
||||||
|
|
||||||
|
# FIXME: conditions don't belong here, use the controller hash instead!
|
||||||
|
# if (1 or getprop("instrumentation/efis/inputs/arpt")) {
|
||||||
|
var icon_apt = apt_grp.createChild("path", name ~ " icon" )
|
||||||
|
.moveTo(-17,0)
|
||||||
|
.arcSmallCW(17,17,0,34,0)
|
||||||
|
.arcSmallCW(17,17,0,-34,0)
|
||||||
|
.close()
|
||||||
|
.setColor(0,0.6,0.85)
|
||||||
|
.setStrokeLineWidth(3);
|
||||||
|
var text_apt = apt_grp.createChild("text", name ~ " label")
|
||||||
|
.setDrawMode( canvas.Text.TEXT )
|
||||||
|
.setTranslation(17,35)
|
||||||
|
.setText(name)
|
||||||
|
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
|
||||||
|
.setColor(0,0.6,0.85)
|
||||||
|
.setFontSize(28);
|
||||||
|
apt_grp.setGeoPosition(lat, lon)
|
||||||
|
.set("z-index",1); # FIXME: this needs to be configurable!!
|
||||||
|
#}
|
||||||
|
|
||||||
|
# draw routines should always return their canvas group to the caller for further processing
|
||||||
|
}
|
11
Nasal/canvas/map/airports-nd.layer
Normal file
11
Nasal/canvas/map/airports-nd.layer
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
var AirportsNDLayer = {};
|
||||||
|
AirportsNDLayer.new = func(group, name) {
|
||||||
|
var m = Layer.new(group, name, AirportsNDModel );
|
||||||
|
m.setDraw(func draw_layer(layer:m, callback:draw_apt, lod:0) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# airport-nd lookup key
|
||||||
|
register_layer("airports-nd", AirportsNDLayer);
|
||||||
|
|
38
Nasal/canvas/map/airports-nd.model
Normal file
38
Nasal/canvas/map/airports-nd.model
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
var AirportsNDModel = {};
|
||||||
|
AirportsNDModel.new = func make(AirportsNDModel, LayerModel);
|
||||||
|
|
||||||
|
AirportsNDModel.init = func {
|
||||||
|
#print("Updating AirportsNDModel");
|
||||||
|
|
||||||
|
me._view.reset();
|
||||||
|
|
||||||
|
var results = positioned.findWithinRange(me._controller.query_range()*2, "airport");
|
||||||
|
var numResults = 0;
|
||||||
|
foreach(result; results) {
|
||||||
|
if (numResults < 50) {
|
||||||
|
var apt = airportinfo(result.id);
|
||||||
|
var runways = apt.runways;
|
||||||
|
var runway_keys = sort(keys(runways),string.icmp);
|
||||||
|
var validApt = 0;
|
||||||
|
foreach(var rwy; runway_keys){
|
||||||
|
var r = runways[rwy];
|
||||||
|
if (r.length > 1890) # Only display suitably large airports
|
||||||
|
validApt = 1;
|
||||||
|
if (result.id == getprop("autopilot/route-manager/destination/airport") or result.id == getprop("autopilot/route-manager/departure/airport"))
|
||||||
|
validApt = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(validApt) {
|
||||||
|
#canvas.draw_apt(me.apt_group, result.lat,result.lon,result.id,i);
|
||||||
|
me.push(result);
|
||||||
|
numResults += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# set RefPos and hdg to apt !!
|
||||||
|
# me._map_handle.setRefPos(apt.lat, apt.lon);
|
||||||
|
|
||||||
|
#TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc)
|
||||||
|
me.notifyView();
|
||||||
|
}
|
||||||
|
|
|
@ -1,27 +1,26 @@
|
||||||
var AirportModel = {};
|
var AirportModel = {};
|
||||||
AirportModel.new = func make(AirportModel, LayerModel);
|
AirportModel.new = func make(AirportModel, LayerModel);
|
||||||
|
|
||||||
# FIXME: Just testing for now: This really shouldn't be part of the core LayerModel, needs to go to "AirportModel" instead
|
# FIXME: Just testing for now: This really shouldn't be part of the core LayerModel, needs to go to "AirportModel" instead
|
||||||
# FIXME: This will get called ONCE for EACH layer that uses the AirportModel, so VERY inefficient ATM! => should be shared among layers
|
# FIXME: This will get called ONCE for EACH layer that uses the AirportModel, so VERY inefficient ATM! => should be shared among layers
|
||||||
|
|
||||||
AirportModel.init = func {
|
AirportModel.init = func {
|
||||||
# print("AirportModel initialized!");
|
me._view.reset();
|
||||||
# me._map_handle.resetLayers();
|
var id = getprop(me._input_property); # HACK: this needs to be handled via the controller - introduce "input_property"
|
||||||
me._view_handle.reset();
|
#print("ID is:", id);
|
||||||
var id = getprop(me._input_property); # HACK: this needs to be handled via the controller - introduce "input_property"
|
(id == "") and return;
|
||||||
#print("ID is:", id);
|
var apt=airportinfo(id); # FIXME: replace with controller call to update the model
|
||||||
(id == "") and return;
|
|
||||||
var apt=airportinfo(id); # FIXME: replace with controller call to update the model
|
foreach(var a; [ apt ]) #FIXME: move to separate method: "populate"
|
||||||
#var airports = findAirportsWithinRange(apt.lat, apt.lon, 10); # HACK: expose the range !!
|
# print("storing:", a.id) and
|
||||||
foreach(var a; [ apt ]) #FIXME: move to separate method: "populate"
|
|
||||||
# print("storing:", a.id) and
|
|
||||||
me.push(a);
|
me.push(a);
|
||||||
#print("Work items in Model:", me.hasData() );
|
#print("Work items in Model:", me.hasData() );
|
||||||
#print("Model updated!!");
|
#print("Model updated!!");
|
||||||
|
|
||||||
# set RefPos and hdg to apt !!
|
# set RefPos and hdg to apt !!
|
||||||
me._map_handle.setRefPos(apt.lat, apt.lon);
|
me._map.setRefPos(apt.lat, apt.lon);
|
||||||
|
|
||||||
#TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc)
|
#TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc)
|
||||||
me.notifyView();
|
me.notifyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
Nasal/canvas/map/altitude-arc.draw
Normal file
32
Nasal/canvas/map/altitude-arc.draw
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
##
|
||||||
|
# pseudo draw routine - rangeNm is passed by the model
|
||||||
|
# needs further work once we adopt the MapStructure framwork
|
||||||
|
# HOWEVER, the alt-arc is an exception in that it's not rendered as
|
||||||
|
# a map item ...
|
||||||
|
|
||||||
|
var draw_altarc = func (group, rangeNm, controller=nil, lod = 0) {
|
||||||
|
# print("drawing alt-arc, range:", rangeNm);
|
||||||
|
|
||||||
|
var altArc = group.createChild("path","alt-arc")
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setColor(0,1,0)
|
||||||
|
.set("clip", "rect(124, 1024, 1024, 0)");
|
||||||
|
|
||||||
|
|
||||||
|
if (abs(getprop("velocities/vertical-speed-fps")) > 10) {
|
||||||
|
altArc.reset();
|
||||||
|
|
||||||
|
var altRangeNm = (getprop("autopilot/settings/target-altitude-ft")-
|
||||||
|
getprop("instrumentation/altimeter/indicated-altitude-ft"))/getprop("velocities/vertical-speed-fps")*
|
||||||
|
getprop("/velocities/groundspeed-kt")*KT2MPS*M2NM;
|
||||||
|
|
||||||
|
if(altRangeNm > 1) {
|
||||||
|
var altRangePx = (256/rangeNm )*altRangeNm;
|
||||||
|
altArc.moveTo(-altRangePx*2.25,0)
|
||||||
|
.arcSmallCW(altRangePx*2.25,altRangePx*2.25,0,altRangePx*4.5,0)
|
||||||
|
.setTranslation(512,824);
|
||||||
|
} # altRangeNm > 1
|
||||||
|
} # fps > 10
|
||||||
|
|
||||||
|
|
||||||
|
} # draw_altarc
|
12
Nasal/canvas/map/altitude-arc.layer
Normal file
12
Nasal/canvas/map/altitude-arc.layer
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
var AltitudeArcLayer = {};
|
||||||
|
AltitudeArcLayer.new = func(group, name, controller=nil) {
|
||||||
|
var m = Layer.new(group, name, AltitudeArcModel );
|
||||||
|
m._model._controller = controller;
|
||||||
|
m.setDraw(func draw_layer(layer:m, callback:canvas.draw_altarc, lod:0) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# airport-nd lookup key
|
||||||
|
register_layer("altitude-arc", AltitudeArcLayer);
|
||||||
|
|
19
Nasal/canvas/map/altitude-arc.model
Normal file
19
Nasal/canvas/map/altitude-arc.model
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
var AltitudeArcModel = {};
|
||||||
|
AltitudeArcModel.new = func make( LayerModel, AltitudeArcModel );
|
||||||
|
|
||||||
|
AltitudeArcModel.init = func {
|
||||||
|
# print("Updating alt-arc model");
|
||||||
|
var rangeNm = me._controller['query_range']();
|
||||||
|
|
||||||
|
me._view.reset();
|
||||||
|
me.push(rangeNm);
|
||||||
|
me.notifyView();
|
||||||
|
|
||||||
|
##
|
||||||
|
# FIXME this should be configurable via the controller
|
||||||
|
# and it should only be running if the predicate (altitude check) is true
|
||||||
|
# for now it'll suffice though
|
||||||
|
settimer(func me.init(), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
34
Nasal/canvas/map/dme.draw
Normal file
34
Nasal/canvas/map/dme.draw
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
###
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
var draw_dme = func (group, dme, controller=nil, lod=0) {
|
||||||
|
var lat = dme.lat;
|
||||||
|
var lon = dme.lon;
|
||||||
|
var name = dme.id;
|
||||||
|
var freq = dme.frequency;
|
||||||
|
|
||||||
|
var dme_grp = group.createChild("group","dme");
|
||||||
|
var icon_dme = dme_grp .createChild("path", name)
|
||||||
|
.moveTo(-15,0)
|
||||||
|
.line(-12.5,-7.5)
|
||||||
|
.line(7.5,-12.5)
|
||||||
|
.line(12.5,7.5)
|
||||||
|
.lineTo(7.5,-12.5)
|
||||||
|
.line(12.5,-7.5)
|
||||||
|
.line(7.5,12.5)
|
||||||
|
.line(-12.5,7.5)
|
||||||
|
.lineTo(15,0)
|
||||||
|
.lineTo(7.5,12.5)
|
||||||
|
.vert(14.5)
|
||||||
|
.horiz(-14.5)
|
||||||
|
.vert(-14.5)
|
||||||
|
.close()
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setColor(0,0.6,0.85);
|
||||||
|
dme_grp.setGeoPosition(lat, lon)
|
||||||
|
.set("z-index",3);
|
||||||
|
|
||||||
|
if( controller != nil and controller['is_tuned'](freq))
|
||||||
|
icon_dme.setColor(0,1,0);
|
||||||
|
}
|
9
Nasal/canvas/map/dme.layer
Normal file
9
Nasal/canvas/map/dme.layer
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
var DMELayer = {};
|
||||||
|
DMELayer.new = func(group,name,controller=nil) {
|
||||||
|
var m=Layer.new(group, name, DMEModel, controller);
|
||||||
|
m.setDraw (func draw_layer(layer:m, callback: draw_dme, lod:0) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_layer("dme", DMELayer);
|
||||||
|
|
22
Nasal/canvas/map/dme.model
Normal file
22
Nasal/canvas/map/dme.model
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
var DMEModel = {};
|
||||||
|
DMEModel.new = func make( LayerModel, DMEModel );
|
||||||
|
|
||||||
|
DMEModel.init = func {
|
||||||
|
me._view.reset();
|
||||||
|
|
||||||
|
if(0) {
|
||||||
|
debug.dump( me._controller );
|
||||||
|
print(typeof(
|
||||||
|
me._controller.query_range()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = positioned.findWithinRange(me._controller.query_range()*2 ,"dme");
|
||||||
|
foreach(result; results) {
|
||||||
|
me.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
me.notifyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
33
Nasal/canvas/map/fix.draw
Normal file
33
Nasal/canvas/map/fix.draw
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
##
|
||||||
|
# draw a single fix symbol
|
||||||
|
#
|
||||||
|
|
||||||
|
var draw_fix = func (group, fix, controller=nil, lod=0) {
|
||||||
|
var lat = fix.lat;
|
||||||
|
var lon = fix.lon;
|
||||||
|
var name = fix.id;
|
||||||
|
|
||||||
|
var fix_grp = group.createChild("group",'fix-'~name); # one group for each fix
|
||||||
|
|
||||||
|
# the fix symbol
|
||||||
|
var icon_fix = fix_grp.createChild("path", "fix-icon-"~ name)
|
||||||
|
.moveTo(-15,15)
|
||||||
|
.lineTo(0,-15)
|
||||||
|
.lineTo(15,15)
|
||||||
|
.close()
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setColor(0,0.6,0.85);
|
||||||
|
|
||||||
|
# the fix label
|
||||||
|
var text_fix = fix_grp.createChild("text", 'fix-label-'~name)
|
||||||
|
.setDrawMode( canvas.Text.TEXT )
|
||||||
|
.setText(name)
|
||||||
|
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
|
||||||
|
.setFontSize(28)
|
||||||
|
.setTranslation(5,25);
|
||||||
|
|
||||||
|
# the fix position
|
||||||
|
fix_grp.setGeoPosition(lat, lon)
|
||||||
|
.set("z-index",3);
|
||||||
|
|
||||||
|
}
|
12
Nasal/canvas/map/fixes.layer
Normal file
12
Nasal/canvas/map/fixes.layer
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
var FixLayer = {};
|
||||||
|
FixLayer.new = func(group,name, controller) {
|
||||||
|
var m=Layer.new(group, name, FixModel);
|
||||||
|
#print("new fix layer, dumping controller:");
|
||||||
|
#debug.dump(controller);
|
||||||
|
m._model._controller = controller; # set up the controller for the model !!!!!
|
||||||
|
m.setDraw (func draw_layer(layer:m, callback: draw_fix, lod:0) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_layer("fixes", FixLayer);
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
var FixModel = {};
|
||||||
|
FixModel.new = func make( LayerModel, FixModel );
|
||||||
|
|
||||||
|
FixModel.init = func {
|
||||||
|
me._view.reset(); # wraps removeAllChildren() ATM
|
||||||
|
|
||||||
|
#fgcommand('profiler-start');
|
||||||
|
#me._view._view.removeAllChildren(); # clear the "real" canvas drawables
|
||||||
|
#fgcommand('profiler-stop');
|
||||||
|
#me.clear();
|
||||||
|
|
||||||
|
#debug.dump( me._controller) ;
|
||||||
|
#print("Query range is:", me._controller['query_range']() );
|
||||||
|
|
||||||
|
var results = positioned.findWithinRange( me._controller['query_range']()*2 ,"fix");
|
||||||
|
foreach(result; results) {
|
||||||
|
me.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#print("query range was:", me._controller['query_range']()*2);
|
||||||
|
#print("total fixes in results/model:", size(results));
|
||||||
|
|
||||||
|
me.notifyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,15 @@
|
||||||
# FIXME: until we have better instancing support for symbols, it would be better to return a functor here
|
# FIXME: until we have better instancing support for symbols, it would be better to return a functor here
|
||||||
# so that symbols are only parsed once
|
# so that symbols are only parsed once
|
||||||
var NAVAID_CACHE = {};
|
var NAVAID_CACHE = {};
|
||||||
|
|
||||||
var draw_navaid = func (group, navaid, lod) {
|
var draw_navaid = func (group, navaid, lod) {
|
||||||
#var group = group.createChild("group", "navaid");
|
#var group = group.createChild("group", "navaid");
|
||||||
DEBUG and print("Drawing navaid:", navaid.id);
|
DEBUG and print("Drawing navaid:", navaid.id);
|
||||||
var symbols = {NDB:"/gui/dialogs/images/ndb_symbol.svg"}; # TODO: add more navaid symbols here
|
var symbols = {NDB:"/gui/dialogs/images/ndb_symbol.svg"}; # TODO: add more navaid symbols here
|
||||||
if (symbols[navaid.type] == nil) return print("Missing svg image for navaid:", navaid.type);
|
if (symbols[navaid.type] == nil) return print("Missing svg image for navaid:", navaid.type);
|
||||||
|
|
||||||
var symbol_navaid = group.createChild("group", "navaid");
|
var symbol_navaid = group.createChild("group", "navaid");
|
||||||
canvas.parsesvg(symbol_navaid, symbols[navaid.type]);
|
canvas.parsesvg(symbol_navaid, symbols[navaid.type]);
|
||||||
symbol_navaid.setGeoPosition(navaid.lat, navaid.lon);
|
symbol_navaid.setGeoPosition(navaid.lat, navaid.lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
var NavLayer = {};
|
var NavLayer = {};
|
||||||
NavLayer.new = func(group,name) {
|
NavLayer.new = func(group,name) {
|
||||||
var m=Layer.new(group, name, NavaidModel);
|
var m=Layer.new(group, name, NavaidModel);
|
||||||
m.setDraw (func draw_layer(layer:m, callback: draw_navaid, lod:0) );
|
m.setDraw (func draw_layer(layer:m, callback: draw_navaid, lod:0) );
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
register_layer("navaids", NavLayer);
|
register_layer("navaids", NavLayer);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
var NavaidModel = {};
|
var NavaidModel = {};
|
||||||
NavaidModel.new = func make(LayerModel, NavaidModel);
|
NavaidModel.new = func make(LayerModel, NavaidModel);
|
||||||
NavaidModel.init = func {
|
NavaidModel.init = func {
|
||||||
me._view_handle.reset();
|
me._view.reset();
|
||||||
var navaids = findNavaidsWithinRange(15);
|
var navaids = findNavaidsWithinRange(15);
|
||||||
foreach(var n; navaids)
|
foreach(var n; navaids)
|
||||||
me.push(n);
|
me.push(n);
|
||||||
me.notifyView();
|
me.notifyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
648
Nasal/canvas/map/navdisplay.mfd
Normal file
648
Nasal/canvas/map/navdisplay.mfd
Normal file
|
@ -0,0 +1,648 @@
|
||||||
|
# ==============================================================================
|
||||||
|
# Boeing Navigation Display by Gijs de Rooy (currently specific to the 744)
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# do we really need to keep track of each drawable here ??
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# pseudo DSL-ish: use these as placeholders in the config hash below
|
||||||
|
var ALWAYS = func 1;
|
||||||
|
var NOTHING = func nil;
|
||||||
|
|
||||||
|
##
|
||||||
|
# so that we only need to update a single line ...
|
||||||
|
#
|
||||||
|
var trigger_update = func(layer) layer._model.init();
|
||||||
|
|
||||||
|
##
|
||||||
|
# TODO: move ND-specific implementation details into this lookup hash
|
||||||
|
# so that other aircraft and ND types can be more easily supported
|
||||||
|
#
|
||||||
|
# any aircraft-specific ND behavior should be wrapped here,
|
||||||
|
# to isolate/decouple things in the generic NavDisplay class
|
||||||
|
#
|
||||||
|
# Note to Gijs: this may look weird and confusing, but it' actually requires
|
||||||
|
# less coding now, and it is now even possible to configure things via a little
|
||||||
|
# XML wrapper
|
||||||
|
# TODO: move this to an XML config file
|
||||||
|
#
|
||||||
|
var NDStyles = {
|
||||||
|
##
|
||||||
|
# this configures the 744 ND to help generalize the NavDisplay class itself
|
||||||
|
'B747-400': {
|
||||||
|
font_mapper: func(family, weight) {
|
||||||
|
if( family == "Liberation Sans" and weight == "normal" )
|
||||||
|
return "LiberationFonts/LiberationSans-Regular.ttf";
|
||||||
|
},
|
||||||
|
|
||||||
|
# where all the symbols are stored
|
||||||
|
# TODO: the SVG image should be moved to the canvas folder
|
||||||
|
# so that it can be shared by other aircraft, i.e. no 744 dependencies
|
||||||
|
# also SVG elements should be renamed to use boeing/airbus prefix
|
||||||
|
# aircraft developers should all be editing the same ND.svg image
|
||||||
|
# the code can deal with the differences now
|
||||||
|
svg_filename: "Aircraft/747-400/Models/Cockpit/Instruments/ND/ND.svg",
|
||||||
|
|
||||||
|
##
|
||||||
|
## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map)
|
||||||
|
##
|
||||||
|
|
||||||
|
layers: [
|
||||||
|
{ name:'fixes', update_on:['toggle_range','toggle_waypoints'], predicate: func(nd, layer) {
|
||||||
|
# print("Running fixes predicate");
|
||||||
|
var visible=nd.get_switch('toggle_waypoints');
|
||||||
|
if(nd.rangeNm() <= 40 and visible and
|
||||||
|
nd.get_switch('toggle_display_mode') == "MAP") {
|
||||||
|
# print("fixes update requested!");
|
||||||
|
trigger_update( layer );
|
||||||
|
} layer._view.setVisible(visible);
|
||||||
|
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of fixes layer
|
||||||
|
|
||||||
|
{ name:'airports-nd', update_on:['toggle_range','toggle_airports','toggle_display_mode'], predicate: func(nd, layer) {
|
||||||
|
# print("Running airports-nd predicate");
|
||||||
|
if (nd.rangeNm() <= 80 and
|
||||||
|
nd.get_switch('toggle_display_mode') == "MAP" ) {
|
||||||
|
trigger_update( layer ); # clear & redraw
|
||||||
|
}
|
||||||
|
layer._view.setVisible( nd.get_switch('toggle_airports') );
|
||||||
|
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of airports layer
|
||||||
|
|
||||||
|
{ name:'vor', update_on:['toggle_range','toggle_stations','toggle_display_mode'], predicate: func(nd, layer) {
|
||||||
|
# print("Running vor layer predicate");
|
||||||
|
if(nd.rangeNm() <= 40 and
|
||||||
|
nd.get_switch('toggle_stations') and
|
||||||
|
nd.get_switch('toggle_display_mode') == "MAP"){
|
||||||
|
trigger_update( layer ); # clear & redraw
|
||||||
|
}
|
||||||
|
layer._view.setVisible( nd.get_switch('toggle_stations') );
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of VOR layer
|
||||||
|
|
||||||
|
{ name:'dme', update_on:['toggle_range','toggle_stations'], predicate: func(nd, layer) {
|
||||||
|
if(nd.rangeNm() <= 40 and
|
||||||
|
nd.get_switch('toggle_stations') and
|
||||||
|
nd.get_switch('toggle_display_mode') == "MAP"){
|
||||||
|
trigger_update( layer ); # clear & redraw
|
||||||
|
}
|
||||||
|
layer._view.setVisible( nd.get_switch('toggle_stations') );
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of DME layer
|
||||||
|
|
||||||
|
{ name:'mp-traffic', update_on:['toggle_range','toggle_traffic'], predicate: func(nd, layer) {
|
||||||
|
trigger_update( layer ); # clear & redraw
|
||||||
|
layer._view.setVisible( 1 ); #nd.get_switch('toggle_traffic')
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of traffic layer
|
||||||
|
|
||||||
|
|
||||||
|
{ name:'runway-nd', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) {
|
||||||
|
var visible = (nd.rangeNm() <= 40 and getprop("autopilot/route-manager/active") ) ;
|
||||||
|
if (visible)
|
||||||
|
trigger_update( layer ); # clear & redraw
|
||||||
|
layer._view.setVisible( visible );
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of airports-nd layer
|
||||||
|
|
||||||
|
{ name:'route', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) {
|
||||||
|
trigger_update( layer ); # clear & redraw
|
||||||
|
layer._view.setVisible( 1 ); #nd.get_switch('toggle_traffic')
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of route layer
|
||||||
|
|
||||||
|
{ name:'altitude-arc', not_a_map:1, update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) {
|
||||||
|
trigger_update( layer ); # clear & redraw
|
||||||
|
layer._view.setVisible( 1 );
|
||||||
|
}, # end of layer update predicate
|
||||||
|
}, # end of alt-arc layer
|
||||||
|
|
||||||
|
## add other layers here, layer names must match the registered names as used in *.layer files for now
|
||||||
|
## this will all change once we're using Philosopher's MapStructure framework
|
||||||
|
|
||||||
|
], # end of vector with configured layers
|
||||||
|
|
||||||
|
# This is where SVG elements are configured by providing "behavior" hashes, i.e. for animations
|
||||||
|
|
||||||
|
# to animate each SVG symbol, specify behavior via callbacks (predicate, and true/false implementation)
|
||||||
|
# SVG identifier, callback etc
|
||||||
|
# TODO: update_on([]), update_mode (update() vs. timers/listeners)
|
||||||
|
# TODO: support putting symbols on specific layers
|
||||||
|
features: [ {
|
||||||
|
# TODO: taOnly doesn't need to use getprop polling in update(), use a listener instead!
|
||||||
|
id: 'taOnly', # the SVG ID
|
||||||
|
impl: { # implementation hash
|
||||||
|
init: func(nd, symbol), # for updateCenter stuff, called during initialization in the ctor
|
||||||
|
predicate: func(nd) getprop("instrumentation/tcas/inputs/mode") == 2, # the condition
|
||||||
|
is_true: func(nd) nd.symbols.taOnly.show(), # if true, run this
|
||||||
|
is_false: func(nd) nd.symbols.taOnly.hide(), # if false, run this
|
||||||
|
}, # end of taOnly behavior/callbacks
|
||||||
|
}, # end of taOnly
|
||||||
|
{
|
||||||
|
id: 'tas',
|
||||||
|
impl: {
|
||||||
|
init: func(nd,symbol),
|
||||||
|
predicate: func(nd) getprop("/velocities/airspeed-kt") > 100,
|
||||||
|
is_true: func(nd) {
|
||||||
|
nd.symbols.tas.setText(sprintf("%3.0f",getprop("/velocities/airspeed-kt") ));
|
||||||
|
nd.symbols.tas.show();
|
||||||
|
},
|
||||||
|
is_false: func(nd) nd.symbols.tas.hide(),
|
||||||
|
}, # end of tas behavior callbacks
|
||||||
|
}, # end of tas hash
|
||||||
|
{
|
||||||
|
id: 'eta',
|
||||||
|
impl: {
|
||||||
|
init: func(nd,symbol),
|
||||||
|
predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil,
|
||||||
|
is_true: func(nd) {
|
||||||
|
var eta=getprop("autopilot/route-manager/wp/eta");
|
||||||
|
var etaWp = split(":",eta);
|
||||||
|
var h = getprop("/sim/time/utc/hour");
|
||||||
|
var m = getprop("/sim/time/utc/minute")+sprintf("%02f",etaWp[0]);
|
||||||
|
var s = getprop("/sim/time/utc/second")+sprintf("%02f",etaWp[1]);
|
||||||
|
nd.symbols.eta.setText(sprintf("%02.0f%02.0f.%02.0fz",h,m,s));
|
||||||
|
nd.symbols.eta.show();
|
||||||
|
},
|
||||||
|
is_false: func(nd) nd.symbols.eta.hide(),
|
||||||
|
}, # of eta.impl
|
||||||
|
}, # of eta
|
||||||
|
{ id:'hdg',
|
||||||
|
impl: {
|
||||||
|
init: func(nd,symbol),
|
||||||
|
predicate: ALWAYS, # always true
|
||||||
|
is_true: func(nd) nd.symbols.hdg.setText(sprintf("%03.0f", nd.aircraft_source.get_hdg() )),
|
||||||
|
is_false: NOTHING,
|
||||||
|
}, # of hdg.impl
|
||||||
|
}, # of hdg
|
||||||
|
|
||||||
|
{ id:'gs',
|
||||||
|
impl: {
|
||||||
|
init: func(nd,symbol),
|
||||||
|
common: func(nd) nd.symbols.gs.setText(sprintf("%3.0f",nd.aircraft_source.get_spd() )),
|
||||||
|
predicate: func(nd) nd.aircraft_source.get_spd() >= 30,
|
||||||
|
is_true: func(nd) {
|
||||||
|
nd.symbols.gs.setFontSize(36);
|
||||||
|
},
|
||||||
|
is_false: func(nd) nd.symbols.gs.setFontSize(52),
|
||||||
|
}, # of gs.impl
|
||||||
|
}, # of gs
|
||||||
|
|
||||||
|
{ id:'compass',
|
||||||
|
impl: {
|
||||||
|
init: func(nd,symbol) nd.getElementById(symbol.id).updateCenter(),
|
||||||
|
common: func(nd) ,
|
||||||
|
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','MAP','PLAN','VOR'] )),
|
||||||
|
is_true: func(nd) {
|
||||||
|
# # orientation/track-magnetic-deg is noisy
|
||||||
|
nd.symbols.compass.setRotation(-nd.aircraft_source.get_hdg() *D2R);
|
||||||
|
nd.symbols.compass.show();
|
||||||
|
},
|
||||||
|
is_false: func(nd) nd.symbols.compass.hide(),
|
||||||
|
}, # of compass.impl
|
||||||
|
}, # of compass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
], # end of vector with features
|
||||||
|
|
||||||
|
|
||||||
|
}, # end of 744 ND style
|
||||||
|
#####
|
||||||
|
##
|
||||||
|
## add support for other aircraft/ND types and styles here (737, 757, 777 etc)
|
||||||
|
##
|
||||||
|
##
|
||||||
|
|
||||||
|
}; # end of NDStyles
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# encapsulate hdg/lat/lon source, so that the ND may also display AI/MP aircraft in a pilot-view at some point (aka stress-testing)
|
||||||
|
#
|
||||||
|
|
||||||
|
var NDSourceDriver = {};
|
||||||
|
NDSourceDriver.new = func {
|
||||||
|
var m = {parents:[NDSourceDriver]};
|
||||||
|
m.get_hdg= func getprop("/orientation/heading-deg");
|
||||||
|
m.get_lat= func getprop("/position/latitude-deg");
|
||||||
|
m.get_lon= func getprop("/position/longitude-deg");
|
||||||
|
m.get_spd= func getprop("/velocities/groundspeed-kt");
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# configure aircraft specific cockpit switches here
|
||||||
|
# these are some defaults, can be overridden when calling NavDisplay.new() -
|
||||||
|
# see the 744 ND.nas file the backend code should never deal directly with
|
||||||
|
# aircraft specific properties using getprop.
|
||||||
|
# To get started implementing your own ND, just copy the switches hash to your
|
||||||
|
# ND.nas file and map the keys to your cockpit properties - and things will just work.
|
||||||
|
|
||||||
|
# TODO: switches are ND specific, so move to the NDStyle hash!
|
||||||
|
|
||||||
|
var default_switches = {
|
||||||
|
'toggle_range': {path: '/inputs/range-nm', value:40, type:'INT'},
|
||||||
|
'toggle_weather': {path: '/inputs/wxr', value:0, type:'BOOL'},
|
||||||
|
'toggle_airports': {path: '/inputs/arpt', value:0, type:'BOOL'},
|
||||||
|
'toggle_stations': {path: '/inputs/sta', value:0, type:'BOOL'},
|
||||||
|
'toggle_waypoints': {path: '/inputs/wpt', value:0, type:'BOOL'},
|
||||||
|
'toggle_position': {path: '/inputs/pos', value:0, type:'BOOL'},
|
||||||
|
'toggle_data': {path: '/inputs/data',value:0, type:'BOOL'},
|
||||||
|
'toggle_terrain': {path: '/inputs/terr',value:0, type:'BOOL'},
|
||||||
|
'toggle_traffic': {path: '/inputs/tcas',value:0, type:'BOOL'},
|
||||||
|
'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'},
|
||||||
|
};
|
||||||
|
|
||||||
|
##
|
||||||
|
# TODO:
|
||||||
|
# - introduce a MFD class (use it also for PFD/EICAS)
|
||||||
|
# - introduce a SGSubsystem class and use it here
|
||||||
|
# - introduce a Boeing NavDisplay class
|
||||||
|
var NavDisplay = {
|
||||||
|
|
||||||
|
# reset handler
|
||||||
|
handle_reinit: func {
|
||||||
|
print("Cleaning up NavDisplay listeners");
|
||||||
|
# shut down all timers and other loops here
|
||||||
|
me.update_timer.stop();
|
||||||
|
foreach(var l; me.listeners)
|
||||||
|
removelistener(l);
|
||||||
|
},
|
||||||
|
|
||||||
|
listen: func(p,c) {
|
||||||
|
append(me.listeners, setlistener(p,c));
|
||||||
|
},
|
||||||
|
|
||||||
|
# listeners for cockpit switches
|
||||||
|
listen_switch: func(s,c) {
|
||||||
|
# print("event setup for: ", id(c));
|
||||||
|
me.listen( me.get_full_switch_path(s), func {
|
||||||
|
# print("listen_switch triggered:", s, " callback id:", id(c) );
|
||||||
|
c();
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
# get the full property path for a given switch
|
||||||
|
get_full_switch_path: func (s) {
|
||||||
|
# debug.dump( me.efis_switches[s] );
|
||||||
|
return me.efis_path ~ me.efis_switches[s].path; # FIXME: should be using props.nas instead of ~
|
||||||
|
},
|
||||||
|
|
||||||
|
# helper method for getting configurable cockpit switches (which are usually different in each aircraft)
|
||||||
|
get_switch: func(s) {
|
||||||
|
var switch = me.efis_switches[s];
|
||||||
|
var path = me.efis_path ~ switch.path ;
|
||||||
|
#print(s,":Getting switch prop:", path);
|
||||||
|
|
||||||
|
return getprop( path );
|
||||||
|
},
|
||||||
|
|
||||||
|
# for creating NDs that are driven by AI traffic instead of the main aircraft (generalization rocks!)
|
||||||
|
connectAI: func(source=nil) {
|
||||||
|
me.aircraft_source = {
|
||||||
|
get_hdg: func source.getNode('orientation/true-heading-deg').getValue(),
|
||||||
|
get_lat: func source.getNode('position/latitude-deg').getValue(),
|
||||||
|
get_lon: func source.getNode('position/longitude-deg').getValue(),
|
||||||
|
get_spd: func source.getNode('velocities/true-airspeed-kt').getValue(),
|
||||||
|
};
|
||||||
|
}, # of connectAI
|
||||||
|
|
||||||
|
# TODO: the ctor should allow customization, for different aircraft
|
||||||
|
# especially properties and SVG files/handles (747, 757, 777 etc)
|
||||||
|
new : func(prop1, switches=default_switches, style='B747-400') {
|
||||||
|
var m = { parents : [NavDisplay]};
|
||||||
|
|
||||||
|
m.listeners=[]; # for cleanup handling
|
||||||
|
m.aircraft_source = NDSourceDriver.new(); # uses the main aircraft as the driver/source (speeds, position, heading)
|
||||||
|
|
||||||
|
m.nd_style = NDStyles[style]; # look up ND specific stuff (file names etc)
|
||||||
|
|
||||||
|
m.radio_list=["instrumentation/comm/frequencies","instrumentation/comm[1]/frequencies",
|
||||||
|
"instrumentation/nav/frequencies","instrumentation/nav[1]/frequencies"];
|
||||||
|
m.mfd_mode_list=["APP","VOR","MAP","PLAN"];
|
||||||
|
|
||||||
|
m.efis_path = prop1;
|
||||||
|
m.efis_switches = switches ;
|
||||||
|
|
||||||
|
# just an alias, to avoid having to rewrite the old code for now
|
||||||
|
m.rangeNm = func m.get_switch('toggle_range');
|
||||||
|
|
||||||
|
m.efis = props.globals.initNode(prop1);
|
||||||
|
m.mfd = m.efis.initNode("mfd");
|
||||||
|
|
||||||
|
# TODO: unify this with switch handling
|
||||||
|
m.mfd_mode_num = m.mfd.initNode("mode-num",2,"INT");
|
||||||
|
m.mfd_display_mode = m.mfd.initNode("display-mode",m.mfd_mode_list[2],"STRING");
|
||||||
|
m.std_mode = m.efis.initNode("inputs/setting-std",0,"BOOL");
|
||||||
|
m.previous_set = m.efis.initNode("inhg-previos",29.92); # watch out typo here, check other files before fixing !
|
||||||
|
m.kpa_mode = m.efis.initNode("inputs/kpa-mode",0,"BOOL");
|
||||||
|
m.kpa_output = m.efis.initNode("inhg-kpa",29.92);
|
||||||
|
m.kpa_prevoutput = m.efis.initNode("inhg-kpa-previous",29.92);
|
||||||
|
m.temp = m.efis.initNode("fixed-temp",0);
|
||||||
|
m.alt_meters = m.efis.initNode("inputs/alt-meters",0,"BOOL");
|
||||||
|
m.fpv = m.efis.initNode("inputs/fpv",0,"BOOL");
|
||||||
|
m.nd_centered = m.efis.initNode("inputs/nd-centered",0,"BOOL");
|
||||||
|
|
||||||
|
m.mins_mode = m.efis.initNode("inputs/minimums-mode",0,"BOOL");
|
||||||
|
m.mins_mode_txt = m.efis.initNode("minimums-mode-text","RADIO","STRING");
|
||||||
|
m.minimums = m.efis.initNode("minimums",250,"INT");
|
||||||
|
m.mk_minimums = props.globals.getNode("instrumentation/mk-viii/inputs/arinc429/decision-height");
|
||||||
|
|
||||||
|
# TODO: these are switches, can be unified with switch handling hash above (eventually):
|
||||||
|
|
||||||
|
m.rh_vor_adf = m.efis.initNode("inputs/rh-vor-adf",0,"INT"); # not yet in switches hash
|
||||||
|
m.lh_vor_adf = m.efis.initNode("inputs/lh-vor-adf",0,"INT"); # not yet in switches hash
|
||||||
|
m.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # ditto
|
||||||
|
|
||||||
|
###
|
||||||
|
# initialize all switches based on the defaults specified in the switch hash
|
||||||
|
#
|
||||||
|
foreach(var switch; keys( m.efis_switches ) )
|
||||||
|
props.globals.initNode
|
||||||
|
( m.get_full_switch_path (switch),
|
||||||
|
m.efis_switches[switch].value,
|
||||||
|
m.efis_switches[switch].type
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return m;
|
||||||
|
},
|
||||||
|
newMFD: func(canvas_group)
|
||||||
|
{
|
||||||
|
|
||||||
|
me.listen("/sim/signals/reinit", func me.handle_reinit() );
|
||||||
|
|
||||||
|
me.update_timer = maketimer(0.05, func me.update() ); # TODO: make interval configurable via ctor
|
||||||
|
me.nd = canvas_group;
|
||||||
|
|
||||||
|
|
||||||
|
# load the specified SVG file into the me.nd group and populate all sub groups
|
||||||
|
|
||||||
|
canvas.parsesvg(me.nd, me.nd_style.svg_filename, {'font-mapper': me.nd_style.font_mapper});
|
||||||
|
|
||||||
|
me.symbols = {}; # storage for SVG elements, to avoid namespace pollution (all SVG elements end up here)
|
||||||
|
|
||||||
|
foreach(var feature; me.nd_style.features ) {
|
||||||
|
# print("Setting up SVG feature:", feature.id);
|
||||||
|
me.symbols[feature.id] = me.nd.getElementById(feature.id);
|
||||||
|
if(contains(feature.impl,'init')) feature.impl.init(me.nd, feature); # call The element's init code (i.e. updateCenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
### this is the "old" method that's less flexible, we want to use the style hash instead (see above)
|
||||||
|
# because things are much better configurable that way
|
||||||
|
# now look up all required SVG elements and initialize member fields using the same name to have a convenient handle
|
||||||
|
foreach(var element; ["wpActiveId","wpActiveDist","wind",
|
||||||
|
"dmeLDist","dmeRDist","vorLId","vorRId",
|
||||||
|
"range","status.wxr","status.wpt",
|
||||||
|
"status.sta","status.arpt"])
|
||||||
|
me.symbols[element] = me.nd.getElementById(element);
|
||||||
|
|
||||||
|
# load elements from vector image, and create instance variables using identical names, and call updateCenter() on each
|
||||||
|
# anything that needs updatecenter called, should be added to the vector here
|
||||||
|
#
|
||||||
|
foreach(var element; ["rotateComp","windArrow","selHdg",
|
||||||
|
"curHdgPtr","staFromL","staToL",
|
||||||
|
"staFromR","staToR"] )
|
||||||
|
me.symbols[element] = me.nd.getElementById(element).updateCenter();
|
||||||
|
|
||||||
|
# this should probably be using Philosopher's new SymbolLayer ?
|
||||||
|
me.map = me.nd.createChild("map","map")
|
||||||
|
.setTranslation(512,824)
|
||||||
|
.set("clip", "rect(124, 1024, 1024, 0)");
|
||||||
|
|
||||||
|
# this callback will be passed onto the model via the controller hash, and used for the positioned queries, to specify max query range:
|
||||||
|
|
||||||
|
var get_range = func me.get_switch('toggle_range');
|
||||||
|
|
||||||
|
# predicate for the draw controller
|
||||||
|
var is_tuned = func(freq) {
|
||||||
|
var nav1=getprop("instrumentation/nav[0]/frequencies/selected-mhz");
|
||||||
|
var nav2=getprop("instrumentation/nav[1]/frequencies/selected-mhz");
|
||||||
|
if (freq == nav1 or freq == nav2) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
# another predicate for the draw controller
|
||||||
|
var get_course_by_freq = func(freq) {
|
||||||
|
if (freq == getprop("instrumentation/nav[0]/frequencies/selected-mhz"))
|
||||||
|
return getprop("instrumentation/nav[0]/radials/selected-deg");
|
||||||
|
else
|
||||||
|
return getprop("instrumentation/nav[1]/radials/selected-deg");
|
||||||
|
}
|
||||||
|
|
||||||
|
var get_current_position = func {
|
||||||
|
return [
|
||||||
|
me.aircraft_source.get_lat(), me.aircraft_source.get_lon()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
# a hash with controller callbacks, will be passed onto draw routines to customize behavior/appearance
|
||||||
|
# the point being that draw routines don't know anything about their frontends (instrument or GUI dialog)
|
||||||
|
# so we need some simple way to communicate between frontend<->backend until we have real controllers
|
||||||
|
# for now, a single controller hash is shared by most layers - unsupported callbacks are simply ignored by the draw files
|
||||||
|
#
|
||||||
|
var controller = { query_range: func get_range(),
|
||||||
|
is_tuned:is_tuned,
|
||||||
|
get_tuned_course:get_course_by_freq,
|
||||||
|
get_position: get_current_position,
|
||||||
|
};
|
||||||
|
|
||||||
|
###
|
||||||
|
# set up various layers, controlled via callbacks in the controller hash
|
||||||
|
# revisit this code once Philosopher's "Smart MVC Symbols/Layers" work is committed and integrated
|
||||||
|
|
||||||
|
# helper / closure generator
|
||||||
|
var make_event_handler = func(predicate, layer) func predicate(me, layer);
|
||||||
|
|
||||||
|
me.layers={}; # storage container for all ND specific layers
|
||||||
|
# look up all required layers as specified per the NDStyle hash and do the initial setup for event handling
|
||||||
|
|
||||||
|
foreach(var layer; me.nd_style.layers) {
|
||||||
|
print("newMFD(): Setting up ND layer:", layer.name);
|
||||||
|
# huge hack for the alt-arc, which is not rendered as a map group, but directly as part of the toplevel ND group
|
||||||
|
var render_target = (!contains(layer,'not_a_map') or !layer.not_a_map) ? me.map : me.nd;
|
||||||
|
var the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller );
|
||||||
|
|
||||||
|
# now register all layer specific notification listeners and their corresponding update predicate/callback
|
||||||
|
# pass the ND instance and the layer handle to the predicate when it is called
|
||||||
|
# so that it can directly access the ND instance and its own layer (without having to know the layer's name)
|
||||||
|
|
||||||
|
var event_handler = make_event_handler(layer.predicate, the_layer);
|
||||||
|
foreach(var event; layer.update_on) {
|
||||||
|
# print("Setting up subscription:", event, " for ", layer.name, " handler id:", id(event_handler) );
|
||||||
|
me.listen_switch(event, event_handler ) ;
|
||||||
|
} # foreach event subscription
|
||||||
|
# and now update/init each layer once by calling its update predicate for initialization
|
||||||
|
event_handler();
|
||||||
|
} # foreach layer
|
||||||
|
|
||||||
|
print("navdisplay.mfd:ND layer setup completed");
|
||||||
|
|
||||||
|
# start the update timer, which makes sure that the update() will be called
|
||||||
|
me.update_timer.start();
|
||||||
|
|
||||||
|
|
||||||
|
# next, radio & autopilot & listeners
|
||||||
|
# TODO: move this to .init field in layers hash or to model files
|
||||||
|
foreach(var n; var radios = [ "instrumentation/nav/frequencies/selected-mhz",
|
||||||
|
"instrumentation/nav[1]/frequencies/selected-mhz"])
|
||||||
|
me.listen(n, func() {
|
||||||
|
me.drawvor();
|
||||||
|
me.drawdme();
|
||||||
|
});
|
||||||
|
# TODO: move this to the route.model
|
||||||
|
me.listen("/autopilot/route-manager/active", func(active) {
|
||||||
|
if(active.getValue()) {
|
||||||
|
me.drawroute();
|
||||||
|
me.drawrunways();
|
||||||
|
} else {
|
||||||
|
print("TODO: navdisplay.mfd: implement route-manager/layer clearing!");
|
||||||
|
#me.route_group.removeAllChildren(); # HACK!
|
||||||
|
}
|
||||||
|
});
|
||||||
|
me.listen("/autopilot/route-manager/current-wp", func(activeWp) {
|
||||||
|
canvas.updatewp( activeWp.getValue() );
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
drawroute: func print("drawroute no longer used!"),
|
||||||
|
drawrunways: func print("drawrunways no longer used!"),
|
||||||
|
|
||||||
|
in_mode:func(switch, modes) foreach(var m; modes) {
|
||||||
|
if (me.get_switch(switch)==m) return 1;
|
||||||
|
else continue;
|
||||||
|
print("not in checked mode");
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
# each model should keep track of when it last got updated, using current lat/lon
|
||||||
|
# in update(), we can then check if the aircraft has traveled more than 0.5-1 nm (depending on selected range)
|
||||||
|
# and update each model accordingly
|
||||||
|
update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft
|
||||||
|
{
|
||||||
|
|
||||||
|
##
|
||||||
|
# important constants
|
||||||
|
var m1 = 111132.92;
|
||||||
|
var m2 = -559.82;
|
||||||
|
var m3 = 1.175;
|
||||||
|
var m4 = -0.0023;
|
||||||
|
var p1 = 111412.84;
|
||||||
|
var p2 = -93.5;
|
||||||
|
var p3 = 0.118;
|
||||||
|
var latNm = 60;
|
||||||
|
var lonNm = 60;
|
||||||
|
|
||||||
|
|
||||||
|
# fgcommand('profiler-start');
|
||||||
|
|
||||||
|
var userHdg = me.aircraft_source.get_hdg();
|
||||||
|
var userTrkMag = me.aircraft_source.get_hdg(); # getprop("orientation/heading-deg"); # orientation/track-magnetic-deg is noisy
|
||||||
|
var userLat = me.aircraft_source.get_lat();
|
||||||
|
var userLon = me.aircraft_source.get_lon();
|
||||||
|
|
||||||
|
# this should only ever happen when testing the experimental AI/MP ND driver hash (not critical)
|
||||||
|
if (!userHdg or !userTrkMag or !userLat or !userLon) {
|
||||||
|
print("aircraft source invalid, returning !");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate length in NM of one degree at current location TODO: expose as methods, for external callbacks
|
||||||
|
var userLatR = userLat*D2R;
|
||||||
|
var userLonR = userLon*D2R;
|
||||||
|
var latlen = m1 + (m2 * math.cos(2 * userLatR)) + (m3 * math.cos(4 * userLatR)) + (m4 * math.cos(6 * userLatR));
|
||||||
|
var lonlen = (p1 * math.cos(userLatR)) + (p2 * math.cos(3 * userLatR)) + (p3 * math.cos(5 * userLatR));
|
||||||
|
latNm = latlen*M2NM; #60 at equator
|
||||||
|
lonNm = lonlen*M2NM; #60 at equator
|
||||||
|
|
||||||
|
me.symbols.windArrow.setRotation((getprop("/environment/wind-from-heading-deg")-userHdg)*D2R);
|
||||||
|
me.symbols.wind.setText(sprintf("%3.0f / %2.0f",getprop("/environment/wind-from-heading-deg"),getprop("/environment/wind-speed-kt")));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if ((var navid0=getprop("instrumentation/nav/nav-id"))!=nil )
|
||||||
|
me.symbols.vorLId.setText(navid0);
|
||||||
|
if ((var navid1=getprop("instrumentation/nav[1]/nav-id"))!=nil )
|
||||||
|
me.symbols.vorRId.setText(navid1);
|
||||||
|
if((var nav0dist=getprop("instrumentation/nav/nav-distance"))!=nil )
|
||||||
|
me.symbols.dmeLDist.setText(sprintf("%3.1f",nav0dist*0.000539));
|
||||||
|
if((var nav1dist=getprop("instrumentation/nav[1]/nav-distance"))!=nil )
|
||||||
|
me.symbols.dmeRDist.setText(sprintf("%3.1f",nav1dist*0.000539));
|
||||||
|
|
||||||
|
me.symbols.range.setText(sprintf("%3.0f",me.rangeNm() ));
|
||||||
|
#rangeNm=rangeNm*2;
|
||||||
|
|
||||||
|
# updates two SVG symbols, should use a listener specified in the config hash
|
||||||
|
if(getprop("/autopilot/route-manager/active")) {
|
||||||
|
me.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id"));
|
||||||
|
me.symbols.wpActiveDist.setText(sprintf("%3.01fNM",getprop("/autopilot/route-manager/wp/dist")));
|
||||||
|
}
|
||||||
|
|
||||||
|
# reposition the map, change heading & range:
|
||||||
|
me.map._node.getNode("ref-lat",1).setDoubleValue(userLat);
|
||||||
|
me.map._node.getNode("ref-lon",1).setDoubleValue(userLon);
|
||||||
|
me.map._node.getNode("hdg",1).setDoubleValue(userHdg); # should also be using a listener for this
|
||||||
|
me.map._node.getNode("range",1).setDoubleValue(me.rangeNm()/2); # avoid this here, use a listener instead
|
||||||
|
|
||||||
|
|
||||||
|
me.symbols.rotateComp.setRotation(-userTrkMag*D2R);
|
||||||
|
|
||||||
|
## these would require additional arguments to be moved to an external config hash currently
|
||||||
|
me.symbols.curHdgPtr.setRotation(userHdg*D2R);
|
||||||
|
me.symbols.selHdg.setRotation(getprop("autopilot/settings/true-heading-deg")*D2R);
|
||||||
|
if (var nav0hdg=getprop("instrumentation/nav/heading-deg") != nil)
|
||||||
|
me.symbols.staFromL.setRotation((nav0hdg-userHdg+180)*D2R);
|
||||||
|
if (var nav0hdg=getprop("instrumentation/nav/heading-deg") != nil)
|
||||||
|
me.symbols.staToL.setRotation((nav0hdg-userHdg)*D2R);
|
||||||
|
if (var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") != nil)
|
||||||
|
me.symbols.staFromR.setRotation((nav1hdg-userHdg+180)*D2R);
|
||||||
|
if (var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") != nil)
|
||||||
|
me.symbols.staToR.setRotation((nav1hdg-userHdg)*D2R);
|
||||||
|
|
||||||
|
|
||||||
|
## run all predicates in the NDStyle hash and evaluate their true/false behavior callbacks
|
||||||
|
## this is in line with the original design, but normally we don't need to getprop/poll here,
|
||||||
|
## using listeners or timers would be more canvas-friendly whenever possible
|
||||||
|
## because running setprop() on any group/canvas element at framerate means that the canvas
|
||||||
|
## will be updated at frame rate too - wasteful ... (check the performance monitor!)
|
||||||
|
|
||||||
|
foreach(var feature; me.nd_style.features ) {
|
||||||
|
|
||||||
|
# for stuff that always needs to be updated
|
||||||
|
if (contains(feature.impl, 'common')) feature.impl.common(me);
|
||||||
|
# conditional stuff
|
||||||
|
if(!contains(feature.impl, 'predicate')) continue; # no conditional stuff
|
||||||
|
if ( var result=feature.impl.predicate(me) ) {
|
||||||
|
# print("Update predicate true for ", feature.id);
|
||||||
|
feature.impl.is_true(me, result); # pass the result to the predicate
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# print("Update predicate false for ", feature.id);
|
||||||
|
feature.impl.is_false( me, result ); # pass the result to the predicate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
## update the status flags shown on the ND (wxr, wpt, arpt, sta)
|
||||||
|
# this could/should be using listeners instead ...
|
||||||
|
|
||||||
|
|
||||||
|
me.symbols['status.wxr'].setVisible( me.get_switch('toggle_weather') );
|
||||||
|
me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints'));
|
||||||
|
me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports'));
|
||||||
|
me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') );
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,15 +1,14 @@
|
||||||
var draw_parking = func(group, apt, lod) {
|
var draw_parking = func(group, apt, lod) {
|
||||||
var group = group.createChild("group", "apt-"~apt.id);
|
var group = group.createChild("group", "apt-"~apt.id);
|
||||||
foreach(var park; apt.parking())
|
foreach(var park; apt.parking()) {
|
||||||
{
|
var icon_park =
|
||||||
var icon_park =
|
group.createChild("text", "parking-" ~ park.name)
|
||||||
group.createChild("text", "parking-" ~ park.name)
|
.setDrawMode( canvas.Text.ALIGNMENT
|
||||||
.setDrawMode( canvas.Text.ALIGNMENT
|
+ canvas.Text.TEXT )
|
||||||
+ canvas.Text.TEXT )
|
.setText(park.name)
|
||||||
.setText(park.name)
|
.setFont("LiberationFonts/LiberationMono-Bold.ttf")
|
||||||
.setFont("LiberationFonts/LiberationMono-Bold.ttf")
|
.setGeoPosition(park.lat, park.lon)
|
||||||
.setGeoPosition(park.lat, park.lon)
|
.setFontSize(15, 1.3);
|
||||||
.setFontSize(15, 1.3);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#TODO: use custom Model/DataProvider
|
#TODO: use custom Model/DataProvider
|
||||||
var ParkingLayer = {}; # make(Layer);
|
var ParkingLayer = {}; # make(Layer);
|
||||||
ParkingLayer.new = func(group, name) {
|
ParkingLayer.new = func(group, name) {
|
||||||
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
||||||
m.setDraw( func draw_layer(layer: m, callback: draw_parking, lod:0 ) );
|
m.setDraw( func draw_layer(layer: m, callback: draw_parking, lod:0 ) );
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
register_layer("parkings", ParkingLayer);
|
register_layer("parkings", ParkingLayer);
|
||||||
|
|
68
Nasal/canvas/map/route.draw
Normal file
68
Nasal/canvas/map/route.draw
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
##
|
||||||
|
# Draw a route with tracks and waypoints (from Gijs' 744 ND.nas code)
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
## FIXME: encapsulate properly
|
||||||
|
var wp = [];
|
||||||
|
var text_wp = [];
|
||||||
|
|
||||||
|
# Change color of active waypoints
|
||||||
|
var updatewp = func(activeWp)
|
||||||
|
{
|
||||||
|
forindex(var i; wp) {
|
||||||
|
if(i == activeWp) {
|
||||||
|
wp[i].setColor(1,0,1);
|
||||||
|
#text_wp[i].setColor(1,0,1);
|
||||||
|
} else {
|
||||||
|
wp[i].setColor(1,1,1);
|
||||||
|
#text_wp[i].setColor(1,1,1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var draw_route = func (group, theroute, controller=nil, lod=0)
|
||||||
|
{
|
||||||
|
#print("draw_route");
|
||||||
|
var route_group = group;
|
||||||
|
|
||||||
|
var route = route_group.createChild("path","route")
|
||||||
|
.setStrokeLineWidth(5)
|
||||||
|
.setColor(1,0,1);
|
||||||
|
|
||||||
|
var cmds = [];
|
||||||
|
var coords = [];
|
||||||
|
|
||||||
|
var fp = flightplan();
|
||||||
|
var fpSize = fp.getPlanSize();
|
||||||
|
|
||||||
|
wp = [];
|
||||||
|
text_wp = [];
|
||||||
|
setsize(wp,fpSize);
|
||||||
|
setsize(text_wp,fpSize);
|
||||||
|
|
||||||
|
# Retrieve route coordinates
|
||||||
|
for (var i=0; i<(fpSize); i += 1)
|
||||||
|
{
|
||||||
|
if (i == 0) {
|
||||||
|
var leg = fp.getWP(1);
|
||||||
|
append(coords,"N"~leg.path()[0].lat);
|
||||||
|
append(coords,"E"~leg.path()[0].lon);
|
||||||
|
append(cmds,2);
|
||||||
|
canvas.drawwp(group, leg.path()[0].lat,leg.path()[0].lon,fp.getWP(0).wp_name,i, wp);
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
var leg = fp.getWP(i);
|
||||||
|
append(coords,"N"~leg.path()[1].lat);
|
||||||
|
append(coords,"E"~leg.path()[1].lon);
|
||||||
|
append(cmds,4);
|
||||||
|
canvas.drawwp(group, leg.path()[1].lat,leg.path()[1].lon,leg.wp_name,i, wp);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update route coordinates
|
||||||
|
debug.dump(cmds);
|
||||||
|
debug.dump(coords);
|
||||||
|
route.setDataGeo(cmds, coords);
|
||||||
|
updatewp(0);
|
||||||
|
}
|
10
Nasal/canvas/map/route.layer
Normal file
10
Nasal/canvas/map/route.layer
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
var RouteLayer = {};
|
||||||
|
|
||||||
|
RouteLayer.new = func(group,name) {
|
||||||
|
var m=Layer.new(group, name, RouteModel);
|
||||||
|
m.setDraw (func draw_layer(layer:m, callback: draw_route, lod:0) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_layer("route", RouteLayer);
|
||||||
|
|
26
Nasal/canvas/map/route.model
Normal file
26
Nasal/canvas/map/route.model
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
|
||||||
|
var RouteModel = {route_monitor:nil};
|
||||||
|
RouteModel.new = func make(LayerModel, RouteModel);
|
||||||
|
|
||||||
|
RouteModel.init = func {
|
||||||
|
me._view.reset();
|
||||||
|
if (!getprop("/autopilot/route-manager/active"))
|
||||||
|
print("Cannot draw route, route manager inactive!") and return;
|
||||||
|
|
||||||
|
print("TODO: route.model is still an empty stub, see route.draw instead");
|
||||||
|
|
||||||
|
## TODO: all the model stuff is still inside the draw file for now, this just ensures that it will be called once
|
||||||
|
foreach(var t; [nil] )
|
||||||
|
me.push(t);
|
||||||
|
|
||||||
|
me.notifyView();
|
||||||
|
|
||||||
|
#FIXME: segfault of the day: use this layer once without a route, and then with a route - and BOOM, need to investigate.
|
||||||
|
|
||||||
|
# TODO: should register a route manager listener here to update itself whenever the route/active WPT changes!
|
||||||
|
# also, if the layer is used in a dialog, the listener should be removed when the dialog is closed
|
||||||
|
if (me.route_monitor == nil) # FIXME: remove this listener durint reinit
|
||||||
|
me.route_monitor=setlistener("/autopilot/route-manager/active", func me.init() ); # this can probably be shared (singleton), because all canvases will be displaying same route ???
|
||||||
|
}
|
||||||
|
|
||||||
|
|
53
Nasal/canvas/map/runway-nd.draw
Normal file
53
Nasal/canvas/map/runway-nd.draw
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
|
||||||
|
var draw_rwy_nd = func (group, rwy, controller=nil, lod=nil) {
|
||||||
|
# print("drawing runways-nd");
|
||||||
|
canvas._draw_rwy_nd(group,rwy.lat,rwy.lon,rwy.length,rwy.width,rwy.heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# TODO: this is not yet a real draw callback ... (wrong signature, not yet integrated)
|
||||||
|
|
||||||
|
var _draw_rwy_nd = func (group, lat, lon, length, width, rwyhdg) {
|
||||||
|
var apt = airportinfo("EHAM");
|
||||||
|
var rwy = apt.runway("18R");
|
||||||
|
|
||||||
|
var crds = [];
|
||||||
|
var coord = geo.Coord.new();
|
||||||
|
width=width*20; # Else rwy is too thin to be visible
|
||||||
|
coord.set_latlon(lat, lon);
|
||||||
|
coord.apply_course_distance(rwyhdg, -14.2*NM2M);
|
||||||
|
append(crds,"N"~coord.lat());
|
||||||
|
append(crds,"E"~coord.lon());
|
||||||
|
coord.apply_course_distance(rwyhdg, 28.4*NM2M+length);
|
||||||
|
append(crds,"N"~coord.lat());
|
||||||
|
append(crds,"E"~coord.lon());
|
||||||
|
icon_rwy = group.createChild("path", "rwy-cl")
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setDataGeo([2,4],crds)
|
||||||
|
.setColor(1,1,1)
|
||||||
|
.setStrokeDashArray([10, 20, 10, 20, 10]);
|
||||||
|
var crds = [];
|
||||||
|
coord.set_latlon(lat, lon);
|
||||||
|
coord.apply_course_distance(rwyhdg + 90, width/2);
|
||||||
|
append(crds,"N"~coord.lat());
|
||||||
|
append(crds,"E"~coord.lon());
|
||||||
|
coord.apply_course_distance(rwyhdg, length);
|
||||||
|
append(crds,"N"~coord.lat());
|
||||||
|
append(crds,"E"~coord.lon());
|
||||||
|
icon_rwy = group.createChild("path", "rwy")
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setDataGeo([2,4],crds)
|
||||||
|
.setColor(1,1,1);
|
||||||
|
var crds = [];
|
||||||
|
coord.apply_course_distance(rwyhdg - 90, width);
|
||||||
|
append(crds,"N"~coord.lat());
|
||||||
|
append(crds,"E"~coord.lon());
|
||||||
|
coord.apply_course_distance(rwyhdg, -length);
|
||||||
|
append(crds,"N"~coord.lat());
|
||||||
|
append(crds,"E"~coord.lon());
|
||||||
|
icon_rwy = group.createChild("path", "rwy")
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setDataGeo([2,4],crds)
|
||||||
|
.setColor(1,1,1);
|
||||||
|
}
|
8
Nasal/canvas/map/runway-nd.layer
Normal file
8
Nasal/canvas/map/runway-nd.layer
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
var RunwayNDLayer = {};
|
||||||
|
RunwayNDLayer.new = func(group, name) {
|
||||||
|
var m=Layer.new(group, name, RunwayNDModel );
|
||||||
|
m.setDraw( func draw_layer(layer: m, callback: draw_rwy_nd, lod:0 ) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
register_layer("runway-nd", RunwayNDLayer);
|
||||||
|
|
26
Nasal/canvas/map/runway-nd.model
Normal file
26
Nasal/canvas/map/runway-nd.model
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
var RunwayNDModel = {};
|
||||||
|
RunwayNDModel.new = func make( LayerModel, RunwayNDModel );
|
||||||
|
|
||||||
|
RunwayNDModel.init = func {
|
||||||
|
me._view.reset();
|
||||||
|
|
||||||
|
# check if RM is active and bail out if not
|
||||||
|
if (!getprop("/autopilot/route-manager/active"))
|
||||||
|
print("runway-nd.model: Cannot access flight plan, route manager inactive!") and return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var desApt = airportinfo(getprop("/autopilot/route-manager/destination/airport"));
|
||||||
|
var depApt = airportinfo(getprop("/autopilot/route-manager/departure/airport"));
|
||||||
|
var desRwy = desApt.runway(getprop("/autopilot/route-manager/destination/runway"));
|
||||||
|
var depRwy = depApt.runway(getprop("/autopilot/route-manager/departure/runway"));
|
||||||
|
|
||||||
|
|
||||||
|
me.push(depRwy);
|
||||||
|
me.push(desRwy);
|
||||||
|
|
||||||
|
|
||||||
|
me.notifyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
|
||||||
#TODO: split: draw_single_runway(pos)
|
#TODO: split: draw_single_runway(pos)
|
||||||
var draw_runways = func(group, apt,lod) {
|
|
||||||
|
var draw_runways = func(group, apt, controller=nil, lod=0) {
|
||||||
DEBUG and print("Drawing runways for:", apt.id);
|
DEBUG and print("Drawing runways for:", apt.id);
|
||||||
# var group = group.createChild("group", "apt-"~apt.id);
|
|
||||||
# group = group.createChild("group", "runways");
|
|
||||||
foreach(var rw1; apt.runwaysWithoutReciprocals())
|
foreach(var rw1; apt.runwaysWithoutReciprocals())
|
||||||
{
|
{
|
||||||
var clr = SURFACECOLORS[rw1.surface];
|
var clr = SURFACECOLORS[rw1.surface];
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
#TODO: use custom Model/DataProvider
|
#TODO: use custom Model/DataProvider
|
||||||
var RunwayLayer = {}; # make(Layer);
|
var RunwayLayer = {}; # make(Layer);
|
||||||
RunwayLayer.new = func(group, name) {
|
RunwayLayer.new = func(group, name) {
|
||||||
# print("Setting up new TestLayer");
|
# print("Setting up new TestLayer");
|
||||||
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
||||||
m.setDraw( func draw_layer(layer: m, callback: draw_runways, lod:0 ) );
|
m.setDraw( func draw_layer(layer: m, callback: draw_runways, lod:0 ) );
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
register_layer("runways", RunwayLayer);
|
register_layer("runways", RunwayLayer);
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,39 @@
|
||||||
var draw_taxiways = func(group, apt, lod) { # TODO: the LOD arg isn't stricly needed here,
|
var draw_taxiways = func(group, apt, lod) { # TODO: the LOD arg isn't stricly needed here,
|
||||||
# the layer is a conventional canvas group, so it can access its map
|
# the layer is a conventional canvas group, so it can access its map
|
||||||
# parent and just read the "range" property to do LOD handling
|
# parent and just read the "range" property to do LOD handling
|
||||||
group.set("z-index",-100) # HACK: we need to encapsulate this
|
group.set("z-index",-100) # HACK: we need to encapsulate this
|
||||||
.set("stroke", "none");
|
.set("stroke", "none");
|
||||||
# var group = group.createChild("group", "apt-"~apt.id); #FIXME: we don't need to use two nested groups for each taxiway - performance?
|
|
||||||
# group = group.createChild("group", "taxiways");
|
|
||||||
# print("drawing taxiways for:", apt.id);
|
|
||||||
# Taxiways drawn first so the runways and parking positions end up on top.
|
|
||||||
|
|
||||||
# Preallocate all paths at once to gain some speed
|
# print("drawing taxiways for:", apt.id);
|
||||||
var taxi_paths = group.createChildren("path", size(apt.taxiways));
|
# Taxiways drawn first so the runways and parking positions end up on top.
|
||||||
var i = 0;
|
|
||||||
foreach(var taxi; apt.taxiways)
|
|
||||||
{
|
|
||||||
var clr = SURFACECOLORS[taxi.surface];
|
|
||||||
if (clr == nil) { clr = SURFACECOLORS[0]};
|
|
||||||
|
|
||||||
var txi = Runway.new(taxi);
|
# Preallocate all paths at once to gain some speed
|
||||||
var beg1 = txi.pointOffCenterline(0, 0.5 * taxi.width);
|
var taxi_paths = group.createChildren("path", size(apt.taxiways));
|
||||||
var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width);
|
var i = 0;
|
||||||
var end1 = txi.pointOffCenterline(taxi.length, 0.5 * taxi.width);
|
foreach(var taxi; apt.taxiways) {
|
||||||
var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width);
|
var clr = SURFACECOLORS[taxi.surface];
|
||||||
|
if (clr == nil) { clr = SURFACECOLORS[0]};
|
||||||
|
|
||||||
taxi_paths[i].setColorFill(clr.r, clr.g, clr.b)
|
var txi = Runway.new(taxi);
|
||||||
.setDataGeo
|
var beg1 = txi.pointOffCenterline(0, 0.5 * taxi.width);
|
||||||
(
|
var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width);
|
||||||
[ canvas.Path.VG_MOVE_TO,
|
var end1 = txi.pointOffCenterline(taxi.length, 0.5 * taxi.width);
|
||||||
canvas.Path.VG_LINE_TO,
|
var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width);
|
||||||
canvas.Path.VG_LINE_TO,
|
|
||||||
canvas.Path.VG_LINE_TO,
|
taxi_paths[i].setColorFill(clr.r, clr.g, clr.b)
|
||||||
canvas.Path.VG_CLOSE_PATH ],
|
.setDataGeo
|
||||||
[ beg1[0], beg1[1],
|
(
|
||||||
beg2[0], beg2[1],
|
[ canvas.Path.VG_MOVE_TO,
|
||||||
end2[0], end2[1],
|
canvas.Path.VG_LINE_TO,
|
||||||
end1[0], end1[1] ]
|
canvas.Path.VG_LINE_TO,
|
||||||
);
|
canvas.Path.VG_LINE_TO,
|
||||||
i += 1;
|
canvas.Path.VG_CLOSE_PATH ],
|
||||||
}
|
[ beg1[0], beg1[1],
|
||||||
|
beg2[0], beg2[1],
|
||||||
|
end2[0], end2[1],
|
||||||
|
end1[0], end1[1] ]
|
||||||
|
);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
#TODO: use custom Model/DataProvider
|
#TODO: use custom Model/DataProvider
|
||||||
var TaxiwayLayer = {}; # make(Layer);
|
var TaxiwayLayer = {}; # make(Layer);
|
||||||
TaxiwayLayer.new = func(group, name) {
|
TaxiwayLayer.new = func(group, name) {
|
||||||
# print("Setting up new TestLayer");
|
# print("Setting up new TestLayer");
|
||||||
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
||||||
m.setDraw( func draw_layer(layer: m, callback: draw_taxiways, lod:0 ) );
|
m.setDraw( func draw_layer(layer: m, callback: draw_taxiways, lod:0 ) );
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
register_layer("taxiways", TaxiwayLayer);
|
register_layer("taxiways", TaxiwayLayer);
|
||||||
|
|
12
Nasal/canvas/map/tcas_arrow_a500.draw
Normal file
12
Nasal/canvas/map/tcas_arrow_a500.draw
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
var draw_tcas_arrow_above_500 = func(group, lod=0) {
|
||||||
|
group.createChild("path")
|
||||||
|
.moveTo(0,-17)
|
||||||
|
.vertTo(17)
|
||||||
|
.lineTo(-10,0)
|
||||||
|
.moveTo(0,17)
|
||||||
|
.lineTo(10,0)
|
||||||
|
.setColor(1,1,1)
|
||||||
|
.setTranslation(25,0)
|
||||||
|
.setStrokeLineWidth(3);
|
||||||
|
|
||||||
|
}
|
14
Nasal/canvas/map/tcas_arrow_b500.draw
Normal file
14
Nasal/canvas/map/tcas_arrow_b500.draw
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
var draw_tcas_arrow_below_500 = func(group) {
|
||||||
|
|
||||||
|
group.createChild("path")
|
||||||
|
.moveTo(0,17)
|
||||||
|
.vertTo(-17)
|
||||||
|
.lineTo(-10,0)
|
||||||
|
.moveTo(0,-17)
|
||||||
|
.lineTo(10,0)
|
||||||
|
.setColor(1,1,1)
|
||||||
|
.setTranslation(25,0)
|
||||||
|
.setStrokeLineWidth(3);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
|
|
||||||
#TODO: use custom Model/DataProvider
|
#TODO: use custom Model/DataProvider
|
||||||
var TestLayer = {}; # make(Layer);
|
var TestLayer = {}; # make(Layer);
|
||||||
TestLayer.new = func(group, name) {
|
TestLayer.new = func(group, name) {
|
||||||
# print("Setting up new TestLayer");
|
# print("Setting up new TestLayer");
|
||||||
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
||||||
m.setDraw( func draw_layer(layer: m, callback: MAP_LAYERS["runways"], lod:0 ) );
|
m.setDraw( func draw_layer(layer: m, callback: MAP_LAYERS["runways"], lod:0 ) );
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
register_layer("airport_test", TestLayer);
|
register_layer("airport_test", TestLayer);
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
var draw_tower = func (group, apt,lod) {
|
var draw_tower = func (group, apt,lod) {
|
||||||
var group = group.createChild("group", "tower");
|
var group = group.createChild("group", "tower");
|
||||||
# TODO: move to map_elements.nas (tower, runway, parking etc)
|
# TODO: move to map_elements.nas (tower, runway, parking etc)
|
||||||
# i.e.: set_element(group, "tower", "style");
|
# i.e.: set_element(group, "tower", "style");
|
||||||
var icon_tower =
|
var icon_tower =
|
||||||
group.createChild("path", "tower")
|
group.createChild("path", "tower")
|
||||||
.setStrokeLineWidth(1)
|
.setStrokeLineWidth(1)
|
||||||
.setScale(1.5)
|
.setScale(1.5)
|
||||||
.setColor(0.2,0.2,1.0)
|
.setColor(0.2,0.2,1.0)
|
||||||
.moveTo(-3, 0)
|
.moveTo(-3, 0)
|
||||||
.vert(-10)
|
.vert(-10)
|
||||||
.line(-3, -10)
|
.line(-3, -10)
|
||||||
.horiz(12)
|
.horiz(12)
|
||||||
.line(-3, 10)
|
.line(-3, 10)
|
||||||
.vert(10);
|
.vert(10);
|
||||||
|
|
||||||
icon_tower.setGeoPosition(apt.tower().lat, apt.tower().lon);
|
icon_tower.setGeoPosition(apt.tower().lat, apt.tower().lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
var TowerLayer = {};
|
var TowerLayer = {};
|
||||||
TowerLayer.new = func(group, name) {
|
TowerLayer.new = func(group, name) {
|
||||||
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!!
|
||||||
m.setDraw( func draw_layer(layer: m, callback: draw_tower, lod:0 ) );
|
m.setDraw( func draw_layer(layer: m, callback: draw_tower, lod:0 ) );
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
register_layer("towers", TowerLayer);
|
register_layer("towers", TowerLayer);
|
||||||
|
|
||||||
|
|
83
Nasal/canvas/map/traffic.draw
Normal file
83
Nasal/canvas/map/traffic.draw
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
var draw_traffic = func(group, traffic, lod=0)
|
||||||
|
{
|
||||||
|
var a = traffic;
|
||||||
|
var tcas_group = group;
|
||||||
|
|
||||||
|
var callsign = a.getNode("callsign").getValue();
|
||||||
|
# print("Drawing traffic for:", callsign );
|
||||||
|
var lat = a.getNode("position/latitude-deg").getValue();
|
||||||
|
var lon = a.getNode("position/longitude-deg").getValue();
|
||||||
|
var alt = a.getNode("position/altitude-ft").getValue();
|
||||||
|
var dist = a.getNode("radar/range-nm").getValue();
|
||||||
|
var threatLvl = a.getNode("tcas/threat-level",1).getValue();
|
||||||
|
var raSense = a.getNode("tcas/ra-sense",1).getValue();
|
||||||
|
var vspeed = a.getNode("velocities/vertical-speed-fps").getValue()*60;
|
||||||
|
var altDiff = alt - getprop("/position/altitude-ft");
|
||||||
|
|
||||||
|
var tcas_grp = tcas_group.createChild("group", callsign);
|
||||||
|
|
||||||
|
var text_tcas = tcas_grp.createChild("text")
|
||||||
|
.setDrawMode( canvas.Text.TEXT )
|
||||||
|
.setText(sprintf("%+02.0f",altDiff/100))
|
||||||
|
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
|
||||||
|
.setColor(1,1,1)
|
||||||
|
.setFontSize(28)
|
||||||
|
.setAlignment("center-center");
|
||||||
|
if (altDiff > 0)
|
||||||
|
text_tcas.setTranslation(0,-40);
|
||||||
|
else
|
||||||
|
text_tcas.setTranslation(0,40);
|
||||||
|
if(vspeed >= 500) {
|
||||||
|
var arrow_tcas = canvas.draw_tcas_arrow_above_500(tcas_grp);
|
||||||
|
} elsif (vspeed < 500) {
|
||||||
|
var arrow_tcas = canvas.draw_tcas_arrow_below_500(tcas_grp);
|
||||||
|
}
|
||||||
|
|
||||||
|
## TODO: threat level symbols should also be moved to *.draw files
|
||||||
|
var icon_tcas = tcas_grp.createChild("path")
|
||||||
|
.setStrokeLineWidth(3);
|
||||||
|
if (threatLvl == 3) {
|
||||||
|
# resolution advisory
|
||||||
|
icon_tcas.moveTo(-17,-17)
|
||||||
|
.horiz(34)
|
||||||
|
.vert(34)
|
||||||
|
.horiz(-34)
|
||||||
|
.close()
|
||||||
|
.setColor(1,0,0)
|
||||||
|
.setColorFill(1,0,0);
|
||||||
|
text_tcas.setColor(1,0,0);
|
||||||
|
arrow_tcas.setColor(1,0,0);
|
||||||
|
} elsif (threatLvl == 2) {
|
||||||
|
# traffic advisory
|
||||||
|
icon_tcas.moveTo(-17,0)
|
||||||
|
.arcSmallCW(17,17,0,34,0)
|
||||||
|
.arcSmallCW(17,17,0,-34,0)
|
||||||
|
.setColor(1,0.5,0)
|
||||||
|
.setColorFill(1,0.5,0);
|
||||||
|
text_tcas.setColor(1,0.5,0);
|
||||||
|
arrow_tcas.setColor(1,0.5,0);
|
||||||
|
} elsif (threatLvl == 1) {
|
||||||
|
# proximate traffic
|
||||||
|
icon_tcas.moveTo(-10,0)
|
||||||
|
.lineTo(0,-17)
|
||||||
|
.lineTo(10,0)
|
||||||
|
.lineTo(0,17)
|
||||||
|
.close()
|
||||||
|
.setColor(1,1,1)
|
||||||
|
.setColorFill(1,1,1);
|
||||||
|
} else {
|
||||||
|
# other traffic
|
||||||
|
icon_tcas.moveTo(-10,0)
|
||||||
|
.lineTo(0,-17)
|
||||||
|
.lineTo(10,0)
|
||||||
|
.lineTo(0,17)
|
||||||
|
.close()
|
||||||
|
.setColor(1,1,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tcas_grp.setGeoPosition(lat, lon)
|
||||||
|
.set("z-index",1);
|
||||||
|
|
||||||
|
|
||||||
|
#settimer(func drawtraffic(group), 2); # updates are handled by the model, not by the view!
|
||||||
|
};
|
11
Nasal/canvas/map/traffic.layer
Normal file
11
Nasal/canvas/map/traffic.layer
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
var MPTrafficLayer = {};
|
||||||
|
MPTrafficLayer.new = func(group,name, controller=nil) {
|
||||||
|
var m=Layer.new(group, name, MPTrafficModel);
|
||||||
|
m._model._controller = controller;
|
||||||
|
m.setDraw (func draw_layer(layer:m, callback: draw_traffic, lod:0) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_layer("mp-traffic", MPTrafficLayer);
|
||||||
|
# TODO: also register AI traffic layer here
|
||||||
|
|
44
Nasal/canvas/map/traffic.model
Normal file
44
Nasal/canvas/map/traffic.model
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
var MPTrafficModel = {};
|
||||||
|
MPTrafficModel.new = func make(LayerModel, MPTrafficModel);
|
||||||
|
|
||||||
|
MPTrafficModel.init = func {
|
||||||
|
var pos = geo.Coord.new(); # FIXME: all of these should be instance variables
|
||||||
|
var myPosition = geo.Coord.new();
|
||||||
|
var myPositionVec = me._controller['get_position']();
|
||||||
|
myPosition.set_latlon( myPositionVec[0], myPositionVec[1]);
|
||||||
|
var max_dist_nm = me._controller['query_range']();
|
||||||
|
|
||||||
|
##
|
||||||
|
# uncomment this for showing MP traffic
|
||||||
|
# var traffic_type = "multiplayer";
|
||||||
|
# and use this for development purposes:
|
||||||
|
var traffic_type = "aircraft";
|
||||||
|
|
||||||
|
#if (traffic_type == "aircraft")
|
||||||
|
# print("INFO: traffic.model is still showing AI traffic instead of MP traffic!");
|
||||||
|
|
||||||
|
me._view.reset(); # hides: removeAllChildren()
|
||||||
|
var traffic = props.globals.initNode("/ai/models/").getChildren( traffic_type );
|
||||||
|
#print("Total traffic:", size(traffic));
|
||||||
|
foreach(var t; traffic) {
|
||||||
|
pos.set_latlon( t.getNode("position/latitude-deg").getValue(),
|
||||||
|
t.getNode("position/longitude-deg").getValue()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pos.distance_to( myPosition ) <= max_dist_nm*NM2M ) {
|
||||||
|
#print("Pushing: ", t.getNode("callsign").getValue() );
|
||||||
|
me.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#print("traffic.model: Query range:", max_dist_nm, " Items:", me.hasData() );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
me.notifyView();
|
||||||
|
|
||||||
|
# update itself FIXME: this needs to be killed by the controller (e.g. when tcas layer is disabled)
|
||||||
|
# and the interval needs to be configurable via the controller
|
||||||
|
# so better use maketimer() here
|
||||||
|
settimer(func me.init(), 2);
|
||||||
|
}
|
||||||
|
|
59
Nasal/canvas/map/vor.draw
Normal file
59
Nasal/canvas/map/vor.draw
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
var draw_vor = func (group, vor, controller=nil, lod = 0) {
|
||||||
|
|
||||||
|
if (0) {
|
||||||
|
if (controller == nil)
|
||||||
|
print("Ooops, VOR controller undefined!");
|
||||||
|
else
|
||||||
|
debug.dump( controller );
|
||||||
|
}
|
||||||
|
|
||||||
|
var lat = vor.lat;
|
||||||
|
var lon = vor.lon;
|
||||||
|
var name = vor.id;
|
||||||
|
var freq = vor.frequency;
|
||||||
|
var range = vor.range_nm;
|
||||||
|
|
||||||
|
# FIXME: Hack - implement a real controller for this!
|
||||||
|
var rangeNm = (controller!=nil) ? controller['query_range']() : 50;
|
||||||
|
|
||||||
|
var vor_grp = group.createChild("group",name);
|
||||||
|
var icon_vor = vor_grp.createChild("path", "vor-icon-" ~ name)
|
||||||
|
.moveTo(-15,0)
|
||||||
|
.lineTo(-7.5,12.5)
|
||||||
|
.lineTo(7.5,12.5)
|
||||||
|
.lineTo(15,0)
|
||||||
|
.lineTo(7.5,-12.5)
|
||||||
|
.lineTo(-7.5,-12.5)
|
||||||
|
.close()
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setColor(0,0.6,0.85);
|
||||||
|
|
||||||
|
# next check if the current VOR is tuned, if so show it
|
||||||
|
# for this to work, we need a controller hash with an "is_tuned" member that points to a callback
|
||||||
|
# (set up by the layer managing this view)
|
||||||
|
# for an example, see the NavDisplay.newMFD() in navdisplay.mfd
|
||||||
|
|
||||||
|
if (controller != nil and controller['is_tuned'](freq/100)) {
|
||||||
|
# print("VOR is tuned:", name);
|
||||||
|
var radius = (range/rangeNm)*345;
|
||||||
|
var range_vor = vor_grp.createChild("path", "range-vor-" ~ name)
|
||||||
|
.moveTo(-radius,0)
|
||||||
|
.arcSmallCW(radius,radius,0,2*radius,0)
|
||||||
|
.arcSmallCW(radius,radius,0,-2*radius,0)
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setStrokeDashArray([5, 15, 5, 15, 5])
|
||||||
|
.setColor(0,1,0);
|
||||||
|
|
||||||
|
var course = controller['get_tuned_course'](freq/100);
|
||||||
|
vor_grp.createChild("path", "radial-vor-" ~ name)
|
||||||
|
.moveTo(0,-radius)
|
||||||
|
.vert(2*radius)
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.setStrokeDashArray([15, 5, 15, 5, 15])
|
||||||
|
.setColor(0,1,0)
|
||||||
|
.setRotation(course*D2R);
|
||||||
|
icon_vor.setColor(0,1,0);
|
||||||
|
}
|
||||||
|
vor_grp.setGeoPosition(lat, lon)
|
||||||
|
.set("z-index",3);
|
||||||
|
}
|
10
Nasal/canvas/map/vor.layer
Normal file
10
Nasal/canvas/map/vor.layer
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
var VORLayer = {};
|
||||||
|
VORLayer.new = func(group,name, controller) {
|
||||||
|
var m=Layer.new(group, name, VORModel );
|
||||||
|
m._controller = controller;
|
||||||
|
m.setDraw (func draw_layer(layer:m, callback: draw_vor, lod:0) );
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
register_layer("vor", VORLayer);
|
||||||
|
|
15
Nasal/canvas/map/vor.model
Normal file
15
Nasal/canvas/map/vor.model
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
var VORModel = {};
|
||||||
|
VORModel.new = func make( LayerModel, VORModel );
|
||||||
|
|
||||||
|
VORModel.init = func {
|
||||||
|
#debug.dump( me._controller );
|
||||||
|
me._view.reset();
|
||||||
|
|
||||||
|
var results = positioned.findWithinRange( me._controller.query_range()*2 ,"vor");
|
||||||
|
foreach(result; results) {
|
||||||
|
me.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
me.notifyView();
|
||||||
|
}
|
||||||
|
|
34
Nasal/canvas/map/waypoint.draw
Normal file
34
Nasal/canvas/map/waypoint.draw
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
##
|
||||||
|
# Draw a waypoint symbol and waypoint name (Gijs' 744 ND.nas code)
|
||||||
|
|
||||||
|
#
|
||||||
|
var drawwp = func (group, lat, lon, name, i, wp) {
|
||||||
|
var wp_group = group.createChild("group","wp");
|
||||||
|
wp[i] = wp_group.createChild("path", "wp-" ~ i)
|
||||||
|
.setStrokeLineWidth(3)
|
||||||
|
.moveTo(0,-25)
|
||||||
|
.lineTo(-5,-5)
|
||||||
|
.lineTo(-25,0)
|
||||||
|
.lineTo(-5,5)
|
||||||
|
.lineTo(0,25)
|
||||||
|
.lineTo(5,5)
|
||||||
|
.lineTo(25,0)
|
||||||
|
.lineTo(5,-5)
|
||||||
|
.setColor(1,1,1)
|
||||||
|
.close();
|
||||||
|
#####
|
||||||
|
# The commented code leads to a segfault when a route is replaced by a new one
|
||||||
|
#####
|
||||||
|
#
|
||||||
|
# text_wp[i] = wp_group.createChild("text", "wp-text-" ~ i)
|
||||||
|
#
|
||||||
|
var text_wps = wp_group.createChild("text", "wp-text-" ~ i)
|
||||||
|
.setDrawMode( canvas.Text.TEXT )
|
||||||
|
.setText(name)
|
||||||
|
.setFont("LiberationFonts/LiberationSans-Regular.ttf")
|
||||||
|
.setFontSize(28)
|
||||||
|
.setTranslation(25,35)
|
||||||
|
.setColor(1,0,1);
|
||||||
|
wp_group.setGeoPosition(lat, lon)
|
||||||
|
.set("z-index",4);
|
||||||
|
};
|
|
@ -444,7 +444,7 @@ var parsesvg = func(group, path, options = nil)
|
||||||
|
|
||||||
var el_src = id_dict[ substr(ref, 1) ];
|
var el_src = id_dict[ substr(ref, 1) ];
|
||||||
if( el_src == nil )
|
if( el_src == nil )
|
||||||
return print("parsesvg: Reference to unknown element (" ~ ref ~ ")");
|
return printlog("info", "parsesvg: Reference to unknown element (" ~ ref ~ ")");
|
||||||
|
|
||||||
# Create new element and copy sub branch from source node
|
# Create new element and copy sub branch from source node
|
||||||
pushElement(el_src._node.getName(), attr['id']);
|
pushElement(el_src._node.getName(), attr['id']);
|
||||||
|
@ -455,7 +455,7 @@ var parsesvg = func(group, path, options = nil)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
print("parsesvg: skipping unknown element '" ~ name ~ "'");
|
printlog("info", "parsesvg: skipping unknown element '" ~ name ~ "'");
|
||||||
skip = level;
|
skip = level;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -324,4 +324,64 @@ var viewer_position = func {
|
||||||
return Coord.new().set_xyz(x, y, z);
|
return Coord.new().set_xyz(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A object to handle differential positioned searches:
|
||||||
|
# searchCmd executes and returns the actual search,
|
||||||
|
# onAdded and onRemoved are callbacks,
|
||||||
|
# and obj is a "me" reference (defaults to "me" in the
|
||||||
|
# caller's namespace).
|
||||||
|
var PositionedSearch = {
|
||||||
|
new: func(searchCmd, onAdded, onRemoved, obj=nil) {
|
||||||
|
return {
|
||||||
|
parents:[PositionedSearch],
|
||||||
|
obj: obj == nil ? caller(1)[0]["me"] : obj,
|
||||||
|
searchCmd: searchCmd,
|
||||||
|
onAdded: onAdded,
|
||||||
|
onRemoved: onRemoved,
|
||||||
|
result: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
_equals: func(a,b) {
|
||||||
|
if (a == nil or b == nil) return 0;
|
||||||
|
return (a == b or a.id == b.id);
|
||||||
|
},
|
||||||
|
condense: func(vec) {
|
||||||
|
var ret = [];
|
||||||
|
foreach (var e; vec)
|
||||||
|
if (e != nil) append(ret, e);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
diff: func(old, new) {
|
||||||
|
var removed = old~[]; #copyvec
|
||||||
|
var added = new~[];
|
||||||
|
# Mark common elements from removed and added:
|
||||||
|
forindex (OUTER; var i; removed)
|
||||||
|
forindex (var j; new)
|
||||||
|
if (me._equals(removed[i], added[j])) {
|
||||||
|
removed[i] = added[j] = nil;
|
||||||
|
continue OUTER;
|
||||||
|
}
|
||||||
|
# And remove those common elements, returning the result:
|
||||||
|
return [new, me.condense(removed), me.condense(added)];
|
||||||
|
},
|
||||||
|
update: func(searchCmd=nil) {
|
||||||
|
if (searchCmd == nil) searchCmd = me.searchCmd;
|
||||||
|
(me.result, var removed, var added) = me.diff(me.result, call(searchCmd, nil, me.obj));
|
||||||
|
foreach (var e; removed)
|
||||||
|
call(me.onRemoved, [e], me.obj);
|
||||||
|
foreach (var e; added)
|
||||||
|
call(me.onAdded, [e], me.obj);
|
||||||
|
},
|
||||||
|
# this is the worst case scenario: switching from 640 to 320 (or vice versa)
|
||||||
|
test: func(from=640, to=320) {
|
||||||
|
var s= geo.PositionedSearch.new(
|
||||||
|
func positioned.findWithinRange(from, 'fix'),
|
||||||
|
func print('added:', arg[0].id),
|
||||||
|
func print('removed:', arg[0].id)
|
||||||
|
);
|
||||||
|
debug.benchmark('Toggle '~from~'nm/'~to~'nm', func {
|
||||||
|
s.update();
|
||||||
|
s.update( func positioned.findWithinRange(to, 'fix') );
|
||||||
|
}); # ~ takes
|
||||||
|
}, # of test
|
||||||
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue