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.
|
||||
#
|
||||
var Map = {
|
||||
df_controller: nil,
|
||||
new: func(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
|
||||
|
@ -442,7 +521,7 @@ var Text = {
|
|||
},
|
||||
# Set alignment
|
||||
#
|
||||
# @param algin String, one of:
|
||||
# @param align String, one of:
|
||||
# left-top
|
||||
# left-center
|
||||
# left-bottom
|
||||
|
@ -653,8 +732,8 @@ var Path = {
|
|||
cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg),
|
||||
cubic: func me.addSegment(me.VG_CUBIC_TO_REL, arg),
|
||||
# Add a smooth quadratic Bézier curve
|
||||
quadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
|
||||
quad: func me.addSegment(me.VG_SQUAD_TO_REL, arg),
|
||||
squadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg),
|
||||
squad: func me.addSegment(me.VG_SQUAD_TO_REL, arg),
|
||||
# Add a smooth cubic Bézier curve
|
||||
scubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, 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
|
||||
# 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
|
||||
#
|
||||
# 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
|
||||
#
|
||||
#
|
||||
# REGRESSIONS: 744 ND: toggle layer on/off, support different dialogs
|
||||
#
|
||||
# ROADMAP: Generalize this further, so that:
|
||||
#
|
||||
# - 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)
|
||||
# - 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 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
|
||||
#
|
||||
|
@ -33,7 +35,7 @@
|
|||
# - 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)
|
||||
# -
|
||||
# -
|
||||
# -
|
||||
#
|
||||
|
||||
|
||||
|
@ -47,28 +49,25 @@ var run_callbacks = func foreach(var c; callbacks) c();
|
|||
var DEBUG=0;
|
||||
if (DEBUG) {
|
||||
var benchmark = debug.benchmark;
|
||||
|
||||
}
|
||||
|
||||
else {
|
||||
} else {
|
||||
var benchmark = func(label, code) code(); # NOP
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
1 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
||||
2 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
||||
3 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
||||
4 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
||||
5 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
||||
# Helipads
|
||||
6 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
||||
7 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
||||
8 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
||||
9 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
||||
0 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
||||
1 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
||||
2 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
||||
3 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
||||
4 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
||||
5 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
||||
# Helipads
|
||||
6 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
|
||||
7 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
|
||||
8 : { type: "turf", r:0.2, g:0.5, b:0.2 },
|
||||
9 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
|
||||
0 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
|
||||
};
|
||||
|
||||
|
||||
|
@ -77,331 +76,197 @@ var SURFACECOLORS = {
|
|||
var draw_layer = func(layer, callback, lod) {
|
||||
var name= layer._view.get("id");
|
||||
# print("Canvas:Draw op triggered"); # just to make sure that we are not adding unnecessary data when checking/unchecking a checkbox
|
||||
if (DEBUG and name=="taxiways") fgcommand("profiler-start"); #without my patch, this is a no op, so no need to disable
|
||||
#if (DEBUG and name=="taxiways") fgcommand("profiler-start"); #without my patch, this is a no op, so no need to disable
|
||||
#print("Work items:", size(layer._model._elements));
|
||||
benchmark("Drawing Layer:"~layer._view.get("id"), func
|
||||
benchmark("Drawing Layer:"~layer._view.get("id"), func
|
||||
foreach(var element; layer._model._elements) {
|
||||
#print(typeof(layer._view));
|
||||
#debug.dump(layer._view);
|
||||
callback(layer._view, element, lod); # ISSUE here
|
||||
callback(layer._view, element, layer._controller, lod); # ISSUE here
|
||||
});
|
||||
if (! layer._model.hasData() ) print("Layer was EMPTY:", name);
|
||||
if (DEBUG and name=="taxiways") fgcommand("profiler-stop");
|
||||
layer._drawn=1; #TODO: this should be encapsulated
|
||||
#if (DEBUG and name=="taxiways") fgcommand("profiler-stop");
|
||||
layer._drawn=1; #TODO: this should be encapsulated
|
||||
}
|
||||
|
||||
# Runway
|
||||
#
|
||||
var Runway = {
|
||||
# Create Runway from hash
|
||||
#
|
||||
# @param rwy Hash containing runway data as returned from
|
||||
# airportinfo().runways[ <runway designator> ]
|
||||
new: func(rwy)
|
||||
{
|
||||
return {
|
||||
parents: [Runway],
|
||||
rwy: rwy
|
||||
};
|
||||
},
|
||||
# Get a point on the runway with the given offset
|
||||
#
|
||||
# @param pos Position along the center line
|
||||
# @param off Offset perpendicular to the center line
|
||||
pointOffCenterline: func(pos, off = 0)
|
||||
{
|
||||
var coord = geo.Coord.new();
|
||||
coord.set_latlon(me.rwy.lat, me.rwy.lon);
|
||||
coord.apply_course_distance(me.rwy.heading, pos);
|
||||
# Create Runway from hash
|
||||
#
|
||||
# @param rwy Hash containing runway data as returned from
|
||||
# airportinfo().runways[ <runway designator> ]
|
||||
new: func(rwy) {
|
||||
return {
|
||||
parents: [Runway],
|
||||
rwy: rwy
|
||||
};
|
||||
},
|
||||
# Get a point on the runway with the given offset
|
||||
#
|
||||
# @param pos Position along the center line
|
||||
# @param off Offset perpendicular to the center line
|
||||
pointOffCenterline: func(pos, off = 0) {
|
||||
var coord = geo.Coord.new();
|
||||
coord.set_latlon(me.rwy.lat, me.rwy.lon);
|
||||
coord.apply_course_distance(me.rwy.heading, pos);
|
||||
|
||||
if( off )
|
||||
coord.apply_course_distance(me.rwy.heading + 90, off);
|
||||
if(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};
|
||||
|
||||
##
|
||||
# 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
|
||||
# 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: };
|
||||
LayerModel.new = func make(LayerModel);
|
||||
LayerModel.clear = func me._elements = [];
|
||||
LayerModel.push = func (e) append(me._elements, e);
|
||||
LayerModel.get = func me._elements;
|
||||
LayerModel.update = func;
|
||||
LayerModel.hasData = func size(me. _elements);
|
||||
LayerModel.setView = func(v) me._view=v;
|
||||
LayerModel.setController = func(c) me._controller=c;
|
||||
|
||||
|
||||
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:, };
|
||||
var LayerModel = {_elements:[], _view:, _controller:{query_range:func 100}, };
|
||||
LayerModel.new = func make(LayerModel);
|
||||
LayerModel.clear = func me._elements = [];
|
||||
LayerModel.push = func (e) append(me._elements, e);
|
||||
LayerModel.get = func me._elements;
|
||||
LayerModel.update = func;
|
||||
LayerModel.hasData = func size(me. _elements);
|
||||
LayerModel.setView = func(v) me._view=v;
|
||||
LayerModel.setController = func(c) me._controller=c;
|
||||
|
||||
|
||||
##
|
||||
# A layer is mapped to a canvas group
|
||||
# Layers are linked to a single boolean property to toggle them on/off
|
||||
var Layer = { _model: ,
|
||||
_view: ,
|
||||
_controller: ,
|
||||
_drawn:0,
|
||||
};
|
||||
## FIXME: this is GUI specific ATM
|
||||
var Layer = {
|
||||
_model: ,
|
||||
_view: ,
|
||||
_controller: ,
|
||||
_drawn:0,
|
||||
};
|
||||
|
||||
Layer.new = func(group, name, model) {
|
||||
Layer.new = func(group, name, model, controller=nil) {
|
||||
#print("Setting up new Layer:", name);
|
||||
var m = make(Layer);
|
||||
var m = make(Layer);
|
||||
m._model = model.new();
|
||||
if (controller!=nil) {
|
||||
m._controller = controller;
|
||||
m._model._controller = controller;
|
||||
}
|
||||
else # use the default controller (query_range for positioned queries =100nm)
|
||||
m._controller = m._model._controller;
|
||||
|
||||
#print("Model name is:", m._model.name);
|
||||
m._view = group.createChild("group",name);
|
||||
m._view = group.createChild("group",name);
|
||||
m._model._view = m;
|
||||
m.name = name; #FIXME: not needed, there's already _view.get("id")
|
||||
return m;
|
||||
}
|
||||
|
||||
Layer.hide = func me._view.setVisible(0);
|
||||
Layer.show = func me._view.setVisible(1);
|
||||
#TODO: Unify toggle and update methods - and support lazy drawing (make it optional!)
|
||||
Layer.toggle = func {
|
||||
Layer.hide = func me._view.setVisible(0);
|
||||
Layer.show = func me._view.setVisible(1);
|
||||
#TODO: Unify toggle and update methods - and support lazy drawing (make it optional!)
|
||||
Layer.toggle = func {
|
||||
# print("Toggling layer");
|
||||
var checkbox = getprop(me.display_layer);
|
||||
if(checkbox and !me._drawn) {
|
||||
if(checkbox and !me._drawn) {
|
||||
# print("Lazy drawing");
|
||||
me.draw();
|
||||
}
|
||||
|
||||
|
||||
#var state= me._view.getBool("visible");
|
||||
#print("Toggle layer visibility ",me.display_layer," checkbox is", checkbox);
|
||||
#print("Layer id is:", me._view.get("id"));
|
||||
#print("Drawn is:", me._drawn);
|
||||
checkbox?me._view.setVisible(1) : me._view.setVisible(0);
|
||||
}
|
||||
Layer.reset = func {
|
||||
me._view.removeAllChildren(); # clear the "real" canvas drawables
|
||||
me._model.clear(); # the vector is used for lazy rendering
|
||||
assert("Model not emptied during layer reset!", me._model.hasData() );
|
||||
me._drawn = 0;
|
||||
}
|
||||
#TODO: Unify toggle and update
|
||||
Layer.update = func {
|
||||
# print("Layer update: Check if layer is visible, if so, draw");
|
||||
if (! getprop(me.display_layer)) return; # checkbox for layer not set
|
||||
if (!me._model.hasData() ) return; # no data available
|
||||
# print("Trying to draw");
|
||||
me.draw();
|
||||
}
|
||||
Layer.reset = func {
|
||||
me._view.removeAllChildren(); # clear the "real" canvas drawables
|
||||
me._model.clear(); # the vector is used for lazy rendering
|
||||
assert("Model not emptied during layer reset!", me._model.hasData() );
|
||||
me._drawn = 0;
|
||||
}
|
||||
#TODO: Unify toggle and update FIXME: GUI specific, not needed for 744 ND.nas
|
||||
Layer.update = func {
|
||||
# print("Layer update: Check if layer is visible, if so, draw");
|
||||
if (contains(me, "display_layer")) #UGLY HACK
|
||||
if (! getprop(me.display_layer)) return; # checkbox for layer not set
|
||||
|
||||
if (!me._model.hasData() ) return; # no data available
|
||||
# print("Trying to draw");
|
||||
me.draw();
|
||||
}
|
||||
|
||||
Layer.setDraw = func(callback) me.draw = callback;
|
||||
Layer.setDraw = func(callback) me.draw = callback;
|
||||
Layer.setController = func(c) me._controller=c; # 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
|
||||
# TODO: Support nested LayeredMaps, where a LayeredMap may contain other LayeredMaps
|
||||
# TODO: use MapBehavior here and move the zoom/refpos methods there, so that map behavior can be easily customized
|
||||
var LayeredMap = { ranges:[],
|
||||
zoom_property:nil, listeners:[],
|
||||
update_property:nil, layers:[],
|
||||
};
|
||||
LayeredMap.new = func(parent, name)
|
||||
return make(LayeredMap, parent.createChild("map",name) );
|
||||
# TODO: use MapBehavior here and move the zoom/refpos methods there, so that map behavior can be easily customized
|
||||
var LayeredMap = {
|
||||
ranges:[],
|
||||
zoom_property:nil, listeners:[],
|
||||
update_property:nil, layers:[],
|
||||
};
|
||||
LayeredMap.new = func(parent, name)
|
||||
return make(LayeredMap, parent.createChild("map",name) );
|
||||
|
||||
LayeredMap.listen = func(p,c) { #FIXME: listening should be managed by each m/v/c separately
|
||||
LayeredMap.listen = func(p,c) { #FIXME: listening should be managed by each m/v/c separately
|
||||
# print("Setting up LayeredMap-managed listener:", p);
|
||||
append(me.listeners, setlistener(p, c));
|
||||
}
|
||||
}
|
||||
|
||||
LayeredMap.initializeLayers = func {
|
||||
LayeredMap.initializeLayers = func {
|
||||
# print("initializing all layers and updating");
|
||||
foreach(var l; me.layers)
|
||||
l.update();
|
||||
}
|
||||
}
|
||||
|
||||
LayeredMap.setRefPos = func(lat, lon) {
|
||||
# print("RefPos set");
|
||||
me._node.getNode("ref-lat", 1).setDoubleValue(lat);
|
||||
me._node.getNode("ref-lon", 1).setDoubleValue(lon);
|
||||
me; # chainable
|
||||
}
|
||||
LayeredMap.setHdg = func(hdg) {
|
||||
me._node.getNode("hdg",1).setDoubleValue(hdg);
|
||||
LayeredMap.setRefPos = func(lat, lon) {
|
||||
# print("RefPos set");
|
||||
me._node.getNode("ref-lat", 1).setDoubleValue(lat);
|
||||
me._node.getNode("ref-lon", 1).setDoubleValue(lon);
|
||||
me; # chainable
|
||||
}
|
||||
}
|
||||
LayeredMap.setHdg = func(hdg) {
|
||||
me._node.getNode("hdg",1).setDoubleValue(hdg);
|
||||
me; # chainable
|
||||
}
|
||||
|
||||
LayeredMap.updateZoom = func {
|
||||
LayeredMap.updateZoom = func {
|
||||
var z = me.zoom_property.getValue() or 0;
|
||||
z = math.max(0, math.min(z, size(me.ranges) - 1));
|
||||
me.zoom_property.setIntValue(z);
|
||||
var zoom = me.ranges[size(me.ranges) - 1 - z];
|
||||
var zoom = me.ranges[size(me.ranges) - 1 - z];
|
||||
# print("Setting zoom range to:", zoom);
|
||||
benchmark("Zooming map:"~zoom, func
|
||||
{
|
||||
benchmark("Zooming map:"~zoom, func {
|
||||
me._node.getNode("range", 1).setDoubleValue(zoom);
|
||||
# TODO update center/limit translation to keep airport always visible
|
||||
});
|
||||
me; #chainable
|
||||
}
|
||||
}
|
||||
|
||||
# this is a huge hack at the moment, we need to encapsulate the setRefPos/setHdg methods, so that they are exposed to XML space
|
||||
#
|
||||
# this is a huge hack at the moment, we need to encapsulate the setRefPos/setHdg methods, so that they are exposed to XML space
|
||||
#
|
||||
LayeredMap.updateState = func {
|
||||
# center map on airport TODO: should be moved to a method and wrapped with a controller so that behavior can be customizeda
|
||||
#var apt = me.layers[0]._model._elements[0];
|
||||
# FIXME:
|
||||
#me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon);
|
||||
|
||||
me.setHdg(0.0);
|
||||
me.updateZoom();
|
||||
# center map on airport TODO: should be moved to a method and wrapped with a controller so that behavior can be customized
|
||||
#var apt = me.layers[0]._model._elements[0];
|
||||
# FIXME:
|
||||
#me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon);
|
||||
|
||||
me.setHdg(0.0);
|
||||
me.updateZoom();
|
||||
}
|
||||
|
||||
#
|
||||
# TODO: this is currently GUI specific and not re-usable for instruments
|
||||
LayeredMap.setupZoom = func(dialog) {
|
||||
var dlgroot = dialog.getNode("features/dialog-root").getValue();#FIXME: GUI specific - needs to be re-implemented for instruments
|
||||
# TODO: this is currently GUI specific and not re-usable for instruments
|
||||
LayeredMap.setupZoom = func(dialog) {
|
||||
var dlgroot = dialog.getNode("features/dialog-root").getValue();#FIXME: GUI specific - needs to be re-implemented for instruments
|
||||
me.zoom_property = props.globals.getNode(dlgroot ~"/"~dialog.getNode("features/range-property").getValue(), 1); #FIXME: this doesn't belong here, need to be in ctor instead !!!
|
||||
ranges=dialog.getNode("features/ranges").getChildren("range");
|
||||
if( size(me.ranges) == 0 )
|
||||
|
@ -413,132 +278,123 @@ LayeredMap.updateState = func {
|
|||
me.listen(me.zoom_property, func me.updateZoom() );
|
||||
me.updateZoom();
|
||||
me; #chainable
|
||||
}
|
||||
LayeredMap.setZoom = func {} #TODO
|
||||
|
||||
LayeredMap.resetLayers = func {
|
||||
|
||||
benchmark("Resetting LayeredMap", func
|
||||
foreach(var l; me.layers) { #TODO: hide all layers, hide map
|
||||
l.reset();
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
LayeredMap.setZoom = func {} #TODO
|
||||
|
||||
LayeredMap.resetLayers = func {
|
||||
benchmark("Resetting LayeredMap",
|
||||
func foreach(var l; me.layers) { #TODO: hide all layers, hide map
|
||||
l.reset();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#FIXME: listener management should be done at the MVC level, for each component - not as part of the LayeredMap!
|
||||
LayeredMap.cleanup_listeners = func {
|
||||
# print("Cleaning up listeners");
|
||||
foreach(var l; me.listeners)
|
||||
removelistener(l);
|
||||
#FIXME: listener management should be done at the MVC level, for each component - not as part of the LayeredMap!
|
||||
LayeredMap.cleanup_listeners = func {
|
||||
# print("Cleaning up listeners");
|
||||
foreach(var l; me.listeners)
|
||||
removelistener(l);
|
||||
# TODO check why me.listeners = []; doesn't work. Maybe this is a Nasal bug
|
||||
# and the old vector is somehow used again.
|
||||
setsize(me.listeners, 0);
|
||||
}
|
||||
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
|
||||
# so that they can be easily associated with GUI properties (checkboxes) or cockpit hotspots
|
||||
# TODO: generalize the XML-parametrization and move it to a helper class
|
||||
|
||||
var GenericMap = { };
|
||||
GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap);
|
||||
GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap);
|
||||
|
||||
GenericMap.setupLayer = func(layer, property) {
|
||||
var l = MAP_LAYERS[layer].new(me, layer); # Layer.new(me, layer);
|
||||
l.display_layer = property; #FIXME: use controller object instead here and this overlaps with update_property
|
||||
#print("Set up layer with toggle property=", property);
|
||||
l._view.setVisible( getprop(property) ) ;
|
||||
append(me.layers, l);
|
||||
return l;
|
||||
}
|
||||
GenericMap.setupLayer = func(layer, property) {
|
||||
var l = MAP_LAYERS[layer].new(me, layer,nil); # Layer.new(me, layer);
|
||||
l.display_layer = property; #FIXME: use controller object instead here and this overlaps with update_property
|
||||
#print("Set up layer with toggle property=", property);
|
||||
l._view.setVisible( getprop(property) ) ;
|
||||
append(me.layers, l);
|
||||
return l;
|
||||
}
|
||||
|
||||
# features are layers - so this will do layer setup and then register listeners for each layer
|
||||
GenericMap.setupFeature = func(layer, property, init ) {
|
||||
var l=me.setupLayer( layer, property );
|
||||
me.listen(property, func l.toggle() ); #TODO: should use the controller object here !
|
||||
# features are layers - so this will do layer setup and then register listeners for each layer
|
||||
GenericMap.setupFeature = func(layer, property, init ) {
|
||||
var l=me.setupLayer( layer, property );
|
||||
me.listen(property, func l.toggle() ); #TODO: should use the controller object here !
|
||||
|
||||
l._model._update_property=property; #TODO: move somewhere else - this is the property that is mapped to the CHECKBOX
|
||||
l._model._view_handle = 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
|
||||
#print("Setting up layer init for property:", init);
|
||||
l._model._update_property=property; #TODO: move somewhere else - this is the property that is mapped to the CHECKBOX
|
||||
l._model._view = l; #FIXME: very crude, set a handle to the view(group), so that the model can notify it (for updates)
|
||||
l._model._map = me; #FIXME: added here so that layers can send update requests to the parent map
|
||||
#print("Setting up layer init for property:", init);
|
||||
|
||||
l._model._input_property = init; # FIXME: init property = input property - needs to be improved!
|
||||
me.listen(init, func l._model.init() ); #TODO: makes sure that the layer's init method for the MODEL is invoked
|
||||
me; #chainable
|
||||
l._model._input_property = init; # FIXME: init property = input property - needs to be improved!
|
||||
me.listen(init, func l._model.init() ); #TODO: makes sure that the layer's init method for the MODEL is invoked
|
||||
me; #chainable
|
||||
};
|
||||
|
||||
# This will read in the config and procedurally instantiate all requested layers and link them to toggle properties
|
||||
# FIXME: this is currently GUI specific and doesn't yet support instrument use, i.e. needs to be generalized further
|
||||
GenericMap.pickupFeatures = func(DIALOG_CANVAS) {
|
||||
var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue();
|
||||
# print("Picking up features for:", DIALOG_CANVAS.getPath() );
|
||||
var layers=DIALOG_CANVAS.getNode("features").getChildren("layer");
|
||||
foreach(var n; layers) {
|
||||
var name = n.getNode("name").getValue();
|
||||
var toggle = n.getNode("property").getValue();
|
||||
var init = n.getNode("init-property").getValue();
|
||||
init = dlgroot ~"/"~init;
|
||||
var property = dlgroot ~"/"~toggle;
|
||||
# print("Adding layer:",n.getNode("name").getValue() );
|
||||
me.setupFeature(name, property, init);
|
||||
}
|
||||
me;
|
||||
# This will read in the config and procedurally instantiate all requested layers and link them to toggle properties
|
||||
# FIXME: this is currently GUI specific and doesn't yet support instrument use, i.e. needs to be generalized further
|
||||
GenericMap.pickupFeatures = func(DIALOG_CANVAS) {
|
||||
var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue();
|
||||
# print("Picking up features for:", DIALOG_CANVAS.getPath() );
|
||||
var layers=DIALOG_CANVAS.getNode("features").getChildren("layer");
|
||||
foreach(var n; layers) {
|
||||
var name = n.getNode("name").getValue();
|
||||
var toggle = n.getNode("property").getValue();
|
||||
var init = n.getNode("init-property").getValue();
|
||||
init = dlgroot ~"/"~init;
|
||||
var property = dlgroot ~"/"~toggle;
|
||||
# print("Adding layer:",n.getNode("name").getValue() );
|
||||
me.setupFeature(name, property, init);
|
||||
}
|
||||
me; #chainable
|
||||
}
|
||||
|
||||
# NOT a method, cmdarg() is no longer meaningful when the canvas nasal block is executed
|
||||
# so this needs to be called in the dialog's OPEN block instead - TODO: generalize
|
||||
#FIXME: move somewhere else, this 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) {
|
||||
var group = globals.gui.findElementByName(cmdarg() , group);
|
||||
var group = globals.gui.findElementByName(cmdarg() , group);
|
||||
|
||||
var layers=dialog.getNode("features").getChildren("layer");
|
||||
var template = dialog.getNode("checkbox-toggle-template");
|
||||
var dlgroot = dialog.getNode("features/dialog-root").getValue();
|
||||
var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue();
|
||||
var i=0;
|
||||
foreach(var n; layers) {
|
||||
var name = n.getNode("name").getValue();
|
||||
var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue();
|
||||
var label = n.getNode("description",1).getValue() or name;
|
||||
var layers=dialog.getNode("features").getChildren("layer");
|
||||
var template = dialog.getNode("checkbox-toggle-template");
|
||||
var dlgroot = dialog.getNode("features/dialog-root").getValue();
|
||||
var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue();
|
||||
var i=0;
|
||||
foreach(var n; layers) {
|
||||
var name = n.getNode("name").getValue();
|
||||
var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue();
|
||||
var label = n.getNode("description",1).getValue() or name;
|
||||
#var query_range = n.getNode("nav-query-range-property").getValue();
|
||||
#print("Query Range:", query_range);
|
||||
|
||||
var default = n.getNode("default",1).getValue();
|
||||
default = (default=="enabled")?1:0;
|
||||
#print("Layer default for", name ," is:", default);
|
||||
setprop(toggle, default); # set the checkbox to its default setting
|
||||
var default = n.getNode("default",1).getValue();
|
||||
default = (default=="enabled")?1:0;
|
||||
#print("Layer default for", name ," is:", default);
|
||||
setprop(toggle, default); # set the checkbox to its default setting
|
||||
|
||||
var hide_checkbox = n.getNode("hide-checkbox",1).getValue();
|
||||
hide_checkbox = (hide_checkbox=="true")?1:0;
|
||||
var hide_checkbox = n.getNode("hide-checkbox",1).getValue();
|
||||
hide_checkbox = (hide_checkbox=="true")?1:0;
|
||||
|
||||
var checkbox = group.getChild("checkbox",i, 1); #FIXME: compute proper offset dynamically, will currently overwrite other existing checkboxes!
|
||||
var checkbox = group.getChild("checkbox",i, 1); #FIXME: compute proper offset dynamically, will currently overwrite other existing checkboxes!
|
||||
|
||||
props.copy(template, checkbox);
|
||||
checkbox.getNode("name").setValue("display-"~name);
|
||||
checkbox.getNode("label").setValue(label);
|
||||
checkbox.getNode("property").setValue(toggle);
|
||||
checkbox.getNode("binding/object-name").setValue("display-"~name);
|
||||
checkbox.getNode("enabled",1).setValue(!hide_checkbox);
|
||||
i+=1;
|
||||
}
|
||||
props.copy(template, checkbox);
|
||||
checkbox.getNode("name").setValue("display-"~name);
|
||||
checkbox.getNode("label").setValue(label);
|
||||
checkbox.getNode("property").setValue(toggle);
|
||||
checkbox.getNode("binding/object-name").setValue("display-"~name);
|
||||
checkbox.getNode("enabled",1).setValue(!hide_checkbox);
|
||||
i+=1;
|
||||
}
|
||||
|
||||
#add zoom buttons procedurally:
|
||||
var template = dialog.getNode("zoom-template");
|
||||
#now add zoom buttons procedurally:
|
||||
var template = dialog.getNode("zoom-template");
|
||||
template.getNode("button[0]/binding[0]/property[0]").setValue(zoom);
|
||||
template.getNode("text[0]/property[0]").setValue(zoom);
|
||||
template.getNode("button[1]/binding[0]/property[0]").setValue(zoom);
|
||||
template.getNode("button[1]/binding[0]/max[0]").setValue( i );
|
||||
template.getNode("button[1]/binding[0]/max[0]").setValue( i );
|
||||
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
|
||||
|
@ -546,48 +402,37 @@ var AirportMap = {};
|
|||
# TODO: adopt real MVC here
|
||||
# FIXME: this must currently be explicitly called by the model, we need to use a wrapper to call it automatically instead!
|
||||
LayerModel.notifyView = func () {
|
||||
# print("View notified");
|
||||
me._view_handle.update(); # update the layer/group
|
||||
me._map_handle.updateState(); # update the map
|
||||
# print("View notified");
|
||||
me._view.update(); # update the layer/group
|
||||
|
||||
### UGLY: disabled for now (probably breaks airport GUI dialog !)
|
||||
### me._map.updateState(); # update the map
|
||||
}
|
||||
|
||||
# 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"
|
||||
|
||||
var MAP_LAYERS = {};
|
||||
var register_layer = func(name, layer) MAP_LAYERS[name]=layer;
|
||||
|
||||
var MVC_FOLDER = getprop("/sim/fg-root") ~ "/Nasal/canvas/map/";
|
||||
var load_modules = func(vec) 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",];
|
||||
load_modules(MODELS);
|
||||
|
||||
var LAYERS = ["runways.layer", "taxiways.layer", "parking.layer", "tower.layer", "navaids.layer","test.layer",];
|
||||
load_modules(LAYERS);
|
||||
|
||||
#TODO: Implement!
|
||||
var CONTROLLERS = [];
|
||||
load_modules(CONTROLLERS);
|
||||
# canvas.MFD = {EFIS:}; # where we'll be storing all MFDs
|
||||
# TODO: should be inside a separate subfolder, i.e. canvas/map/mfd
|
||||
load_modules( files_with('.mfd'), 'canvas' );
|
||||
|
|
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 = {};
|
||||
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: This will get called ONCE for EACH layer that uses the AirportModel, so VERY inefficient ATM! => should be shared among layers
|
||||
|
||||
AirportModel.init = func {
|
||||
# print("AirportModel initialized!");
|
||||
# me._map_handle.resetLayers();
|
||||
me._view_handle.reset();
|
||||
var id = getprop(me._input_property); # HACK: this needs to be handled via the controller - introduce "input_property"
|
||||
#print("ID is:", id);
|
||||
(id == "") and return;
|
||||
var apt=airportinfo(id); # FIXME: replace with controller call to update the model
|
||||
#var airports = findAirportsWithinRange(apt.lat, apt.lon, 10); # HACK: expose the range !!
|
||||
foreach(var a; [ apt ]) #FIXME: move to separate method: "populate"
|
||||
# print("storing:", a.id) and
|
||||
me._view.reset();
|
||||
var id = getprop(me._input_property); # HACK: this needs to be handled via the controller - introduce "input_property"
|
||||
#print("ID is:", id);
|
||||
(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"
|
||||
# print("storing:", a.id) and
|
||||
me.push(a);
|
||||
#print("Work items in Model:", me.hasData() );
|
||||
#print("Model updated!!");
|
||||
#print("Work items in Model:", me.hasData() );
|
||||
#print("Model updated!!");
|
||||
|
||||
# set RefPos and hdg to apt !!
|
||||
me._map_handle.setRefPos(apt.lat, apt.lon);
|
||||
# set RefPos and hdg to apt !!
|
||||
me._map.setRefPos(apt.lat, apt.lon);
|
||||
|
||||
#TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc)
|
||||
me.notifyView();
|
||||
#TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc)
|
||||
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
|
||||
# so that symbols are only parsed once
|
||||
var NAVAID_CACHE = {};
|
||||
|
||||
var draw_navaid = func (group, navaid, lod) {
|
||||
#var group = group.createChild("group", "navaid");
|
||||
DEBUG and print("Drawing navaid:", navaid.id);
|
||||
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);
|
||||
|
||||
var symbol_navaid = group.createChild("group", "navaid");
|
||||
canvas.parsesvg(symbol_navaid, symbols[navaid.type]);
|
||||
symbol_navaid.setGeoPosition(navaid.lat, navaid.lon);
|
||||
#var group = group.createChild("group", "navaid");
|
||||
DEBUG and print("Drawing navaid:", navaid.id);
|
||||
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);
|
||||
|
||||
var symbol_navaid = group.createChild("group", "navaid");
|
||||
canvas.parsesvg(symbol_navaid, symbols[navaid.type]);
|
||||
symbol_navaid.setGeoPosition(navaid.lat, navaid.lon);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
var NavLayer = {};
|
||||
NavLayer.new = func(group,name) {
|
||||
var m=Layer.new(group, name, NavaidModel);
|
||||
m.setDraw (func draw_layer(layer:m, callback: draw_navaid, lod:0) );
|
||||
return m;
|
||||
NavLayer.new = func(group,name) {
|
||||
var m=Layer.new(group, name, NavaidModel);
|
||||
m.setDraw (func draw_layer(layer:m, callback: draw_navaid, lod:0) );
|
||||
return m;
|
||||
}
|
||||
|
||||
register_layer("navaids", NavLayer);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
var NavaidModel = {};
|
||||
NavaidModel.new = func make(LayerModel, NavaidModel);
|
||||
NavaidModel.init = func {
|
||||
me._view_handle.reset();
|
||||
var navaids = findNavaidsWithinRange(15);
|
||||
foreach(var n; navaids)
|
||||
me.push(n);
|
||||
me.notifyView();
|
||||
NavaidModel.new = func make(LayerModel, NavaidModel);
|
||||
NavaidModel.init = func {
|
||||
me._view.reset();
|
||||
var navaids = findNavaidsWithinRange(15);
|
||||
foreach(var n; navaids)
|
||||
me.push(n);
|
||||
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 group = group.createChild("group", "apt-"~apt.id);
|
||||
foreach(var park; apt.parking())
|
||||
{
|
||||
var icon_park =
|
||||
group.createChild("text", "parking-" ~ park.name)
|
||||
.setDrawMode( canvas.Text.ALIGNMENT
|
||||
+ canvas.Text.TEXT )
|
||||
.setText(park.name)
|
||||
.setFont("LiberationFonts/LiberationMono-Bold.ttf")
|
||||
.setGeoPosition(park.lat, park.lon)
|
||||
.setFontSize(15, 1.3);
|
||||
}
|
||||
var group = group.createChild("group", "apt-"~apt.id);
|
||||
foreach(var park; apt.parking()) {
|
||||
var icon_park =
|
||||
group.createChild("text", "parking-" ~ park.name)
|
||||
.setDrawMode( canvas.Text.ALIGNMENT
|
||||
+ canvas.Text.TEXT )
|
||||
.setText(park.name)
|
||||
.setFont("LiberationFonts/LiberationMono-Bold.ttf")
|
||||
.setGeoPosition(park.lat, park.lon)
|
||||
.setFontSize(15, 1.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#TODO: use custom Model/DataProvider
|
||||
var ParkingLayer = {}; # make(Layer);
|
||||
ParkingLayer.new = func(group, name) {
|
||||
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 ) );
|
||||
return m;
|
||||
ParkingLayer.new = func(group, name) {
|
||||
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 ) );
|
||||
return m;
|
||||
}
|
||||
|
||||
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)
|
||||
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);
|
||||
# var group = group.createChild("group", "apt-"~apt.id);
|
||||
# group = group.createChild("group", "runways");
|
||||
|
||||
foreach(var rw1; apt.runwaysWithoutReciprocals())
|
||||
{
|
||||
var clr = SURFACECOLORS[rw1.surface];
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#TODO: use custom Model/DataProvider
|
||||
var RunwayLayer = {}; # make(Layer);
|
||||
RunwayLayer.new = func(group, name) {
|
||||
# print("Setting up new TestLayer");
|
||||
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 ) );
|
||||
return m;
|
||||
RunwayLayer.new = func(group, name) {
|
||||
# print("Setting up new TestLayer");
|
||||
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 ) );
|
||||
return m;
|
||||
}
|
||||
register_layer("runways", RunwayLayer);
|
||||
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
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
|
||||
# parent and just read the "range" property to do LOD handling
|
||||
group.set("z-index",-100) # HACK: we need to encapsulate this
|
||||
.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.
|
||||
group.set("z-index",-100) # HACK: we need to encapsulate this
|
||||
.set("stroke", "none");
|
||||
|
||||
# Preallocate all paths at once to gain some speed
|
||||
var taxi_paths = group.createChildren("path", size(apt.taxiways));
|
||||
var i = 0;
|
||||
foreach(var taxi; apt.taxiways)
|
||||
{
|
||||
var clr = SURFACECOLORS[taxi.surface];
|
||||
if (clr == nil) { clr = SURFACECOLORS[0]};
|
||||
# print("drawing taxiways for:", apt.id);
|
||||
# Taxiways drawn first so the runways and parking positions end up on top.
|
||||
|
||||
var txi = Runway.new(taxi);
|
||||
var beg1 = txi.pointOffCenterline(0, 0.5 * taxi.width);
|
||||
var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width);
|
||||
var end1 = txi.pointOffCenterline(taxi.length, 0.5 * taxi.width);
|
||||
var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width);
|
||||
# Preallocate all paths at once to gain some speed
|
||||
var taxi_paths = group.createChildren("path", size(apt.taxiways));
|
||||
var i = 0;
|
||||
foreach(var taxi; apt.taxiways) {
|
||||
var clr = SURFACECOLORS[taxi.surface];
|
||||
if (clr == nil) { clr = SURFACECOLORS[0]};
|
||||
|
||||
taxi_paths[i].setColorFill(clr.r, clr.g, clr.b)
|
||||
.setDataGeo
|
||||
(
|
||||
[ canvas.Path.VG_MOVE_TO,
|
||||
canvas.Path.VG_LINE_TO,
|
||||
canvas.Path.VG_LINE_TO,
|
||||
canvas.Path.VG_LINE_TO,
|
||||
canvas.Path.VG_CLOSE_PATH ],
|
||||
[ beg1[0], beg1[1],
|
||||
beg2[0], beg2[1],
|
||||
end2[0], end2[1],
|
||||
end1[0], end1[1] ]
|
||||
);
|
||||
i += 1;
|
||||
}
|
||||
var txi = Runway.new(taxi);
|
||||
var beg1 = txi.pointOffCenterline(0, 0.5 * taxi.width);
|
||||
var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width);
|
||||
var end1 = txi.pointOffCenterline(taxi.length, 0.5 * taxi.width);
|
||||
var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width);
|
||||
|
||||
taxi_paths[i].setColorFill(clr.r, clr.g, clr.b)
|
||||
.setDataGeo
|
||||
(
|
||||
[ canvas.Path.VG_MOVE_TO,
|
||||
canvas.Path.VG_LINE_TO,
|
||||
canvas.Path.VG_LINE_TO,
|
||||
canvas.Path.VG_LINE_TO,
|
||||
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
|
||||
var TaxiwayLayer = {}; # make(Layer);
|
||||
TaxiwayLayer.new = func(group, name) {
|
||||
# print("Setting up new TestLayer");
|
||||
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 ) );
|
||||
return m;
|
||||
TaxiwayLayer.new = func(group, name) {
|
||||
# print("Setting up new TestLayer");
|
||||
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 ) );
|
||||
return m;
|
||||
}
|
||||
|
||||
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
|
||||
var TestLayer = {}; # make(Layer);
|
||||
TestLayer.new = func(group, name) {
|
||||
# print("Setting up new TestLayer");
|
||||
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 ) );
|
||||
return m;
|
||||
TestLayer.new = func(group, name) {
|
||||
# print("Setting up new TestLayer");
|
||||
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 ) );
|
||||
return m;
|
||||
}
|
||||
|
||||
register_layer("airport_test", TestLayer);
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
var draw_tower = func (group, apt,lod) {
|
||||
var group = group.createChild("group", "tower");
|
||||
# TODO: move to map_elements.nas (tower, runway, parking etc)
|
||||
# i.e.: set_element(group, "tower", "style");
|
||||
var icon_tower =
|
||||
group.createChild("path", "tower")
|
||||
.setStrokeLineWidth(1)
|
||||
.setScale(1.5)
|
||||
.setColor(0.2,0.2,1.0)
|
||||
.moveTo(-3, 0)
|
||||
.vert(-10)
|
||||
.line(-3, -10)
|
||||
.horiz(12)
|
||||
.line(-3, 10)
|
||||
.vert(10);
|
||||
|
||||
icon_tower.setGeoPosition(apt.tower().lat, apt.tower().lon);
|
||||
var group = group.createChild("group", "tower");
|
||||
# TODO: move to map_elements.nas (tower, runway, parking etc)
|
||||
# i.e.: set_element(group, "tower", "style");
|
||||
var icon_tower =
|
||||
group.createChild("path", "tower")
|
||||
.setStrokeLineWidth(1)
|
||||
.setScale(1.5)
|
||||
.setColor(0.2,0.2,1.0)
|
||||
.moveTo(-3, 0)
|
||||
.vert(-10)
|
||||
.line(-3, -10)
|
||||
.horiz(12)
|
||||
.line(-3, 10)
|
||||
.vert(10);
|
||||
|
||||
icon_tower.setGeoPosition(apt.tower().lat, apt.tower().lon);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
var TowerLayer = {};
|
||||
TowerLayer.new = func(group, name) {
|
||||
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 ) );
|
||||
return m;
|
||||
TowerLayer.new = func(group, name) {
|
||||
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 ) );
|
||||
return m;
|
||||
}
|
||||
|
||||
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) ];
|
||||
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
|
||||
pushElement(el_src._node.getName(), attr['id']);
|
||||
|
@ -455,7 +455,7 @@ var parsesvg = func(group, path, options = nil)
|
|||
}
|
||||
else
|
||||
{
|
||||
print("parsesvg: skipping unknown element '" ~ name ~ "'");
|
||||
printlog("info", "parsesvg: skipping unknown element '" ~ name ~ "'");
|
||||
skip = level;
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -324,4 +324,64 @@ var viewer_position = func {
|
|||
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…
Reference in a new issue