1
0
Fork 0

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:
Gijs de Rooy 2013-12-01 13:29:22 +01:00
parent 315e22cad0
commit a9576e8c8d
50 changed files with 2441 additions and 528 deletions

View 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

View file

@ -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),

View file

@ -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' );

View 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
};

View 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");

View 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);
};

View 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();
};

View 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
}

View 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);

View 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();
}

View file

@ -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();
}

View 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

View 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);

View 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
View 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);
}

View 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);

View 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
View 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);
}

View 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);

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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();
}

View 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') );
}
};

View file

@ -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);
}
}

View file

@ -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);

View 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);
}

View 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);

View 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 ???
}

View 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);
}

View 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);

View 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();
}

View file

@ -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];

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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);

View 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);
}

View 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);
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View 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!
};

View 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

View 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
View 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);
}

View 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);

View 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();
}

View 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);
};

View file

@ -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;
}

View file

@ -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
};