Canvas ND: use MapStructure for TFC, other misc.
Implement traffic in MapStructure and use it. Various other hacks and/or cleanup. Feedback required on whether this is a lot better than before. Also partially revert 9c018d94c4d88dad7476ec250fa3b52024526f4b to add feature to geo.PositionedSearch: it me._equals is overridden then the old mechanism is used instead of the new C++ function, so that the custom equality can be used. (In particular for the Fixes with the TrafficModel class).
This commit is contained in:
parent
0ee6a5c71e
commit
0d4a86e3d4
8 changed files with 295 additions and 32 deletions
|
@ -170,7 +170,7 @@ Symbol.Controller = {
|
|||
# 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
|
||||
getpos: func(model) , # default provided below
|
||||
}; # of Symbol.Controller
|
||||
|
||||
var getpos_fromghost = func(positioned_g)
|
||||
|
@ -187,6 +187,11 @@ Symbol.Controller.getpos = func(obj) {
|
|||
if (typeof(obj) == 'hash')
|
||||
if (isa(obj, geo.Coord))
|
||||
return obj.latlon();
|
||||
if (isa(obj, props.Node))
|
||||
return [
|
||||
obj.getValue("position/latitude-deg") or obj.getValue("latitude-deg"),
|
||||
obj.getValue("position/longitude-deg") or obj.getValue("longitude-deg")
|
||||
];
|
||||
if (contains(obj,'lat') and contains(obj,'lon'))
|
||||
return [obj.lat, obj.lon];
|
||||
|
||||
|
@ -194,6 +199,22 @@ Symbol.Controller.getpos = func(obj) {
|
|||
die("no suitable getpos() found! Of type: "~typeof(obj));
|
||||
};
|
||||
|
||||
Symbol.Controller.equals = func(l, r) {
|
||||
if (l == r) return 1;
|
||||
var t = typeof(l);
|
||||
if (t == 'ghost')
|
||||
return 0;#l.id == r.id;
|
||||
if (t == 'hash')
|
||||
if (isa(l, props.Node))
|
||||
return l.equals(r);
|
||||
else {
|
||||
foreach (var k; keys(l))
|
||||
if (l[k] != r[k]) return 0;
|
||||
return 1;
|
||||
}
|
||||
die("bad types");
|
||||
};
|
||||
|
||||
|
||||
var assert_m = func(hash, member)
|
||||
if (!contains(hash, member))
|
||||
|
@ -306,6 +327,7 @@ var SymbolLayer = {
|
|||
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: [],
|
||||
};
|
||||
m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m);
|
||||
# FIXME: hack to expose type of layer:
|
||||
if (caller(1)[1] == Map.addLayer) {
|
||||
var this_type = caller(1)[0].type_arg;
|
||||
|
@ -323,7 +345,6 @@ var SymbolLayer = {
|
|||
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;
|
||||
},
|
||||
|
@ -338,12 +359,13 @@ var SymbolLayer = {
|
|||
foreach (var e; me.list)
|
||||
e.del();
|
||||
},
|
||||
findsym: func(positioned_g, del=0) {
|
||||
findsym: func(model, del=0) {
|
||||
forindex (var i; me.list) {
|
||||
var e = me.list[i];
|
||||
if (geo.PositionedSearch._equals(e.model, positioned_g)) {
|
||||
if (Symbol.Controller.equals(e.model, model)) {
|
||||
if (del) {
|
||||
# Remove this element from the list
|
||||
# TODO: maybe C function for this? extend pop() to accept index?
|
||||
var prev = subvec(me.list, 0, i);
|
||||
var next = subvec(me.list, i+1);
|
||||
me.list = prev~next;
|
||||
|
@ -355,11 +377,11 @@ var SymbolLayer = {
|
|||
},
|
||||
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(),
|
||||
onAdded: func(model)
|
||||
append(me.list, Symbol.new(me.type, me.group, model)),
|
||||
# Removes a symbol.
|
||||
onRemoved: func(model)
|
||||
me.findsym(model, 1).del(),
|
||||
}; # of SymbolLayer
|
||||
|
||||
# Class to manage controlling a #SymbolLayer.
|
||||
|
@ -397,7 +419,7 @@ var CompassLayer = {
|
|||
var AltitudeArcLayer = {
|
||||
};
|
||||
|
||||
load_MapStructure = func {
|
||||
var load_MapStructure = func {
|
||||
Map.Controller = {
|
||||
# Static/singleton:
|
||||
registry: {},
|
||||
|
@ -453,7 +475,7 @@ load_MapStructure = func {
|
|||
load(FG_ROOT~"/Nasal/canvas/map/"~name~".scontroller", name);
|
||||
}
|
||||
|
||||
foreach( var name; ['VOR','FIX','NDB','DME','WPT'] )
|
||||
foreach( var name; ['VOR','FIX','NDB','DME','WPT','TFC'] )
|
||||
load_deps( name );
|
||||
load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", name);
|
||||
|
||||
|
|
98
Nasal/canvas/map/TFC.lcontroller
Normal file
98
Nasal/canvas/map/TFC.lcontroller
Normal file
|
@ -0,0 +1,98 @@
|
|||
# Class things:
|
||||
var name = 'TFC';
|
||||
var parents = [SymbolLayer.Controller];
|
||||
var __self__ = caller(0)[0];
|
||||
SymbolLayer.Controller.add(name, __self__);
|
||||
SymbolLayer.add(name, {
|
||||
parents: [SymbolLayer],
|
||||
type: name, # Symbol type
|
||||
df_controller: __self__, # controller to use by default -- this one
|
||||
});
|
||||
|
||||
var model_root = props.globals.getNode("/ai/models/");
|
||||
|
||||
var new = func(layer) {
|
||||
var m = {
|
||||
parents: [__self__],
|
||||
layer: layer,
|
||||
listeners: [],
|
||||
query_range_nm: 25,
|
||||
};
|
||||
# Listen to ai model events
|
||||
append(m.listeners, setlistener(
|
||||
model_root.getNode("model-added"), func(n) {
|
||||
var node = props.globals.getNode(n.getValue());
|
||||
var name = node.getName();
|
||||
if (name == "aircraft" or name == "multiplayer")
|
||||
layer.onAdded(TrafficModel.new(node));
|
||||
}
|
||||
));
|
||||
append(m.listeners, setlistener(
|
||||
model_root.getNode("model-removed"), func(n) {
|
||||
var node = props.globals.getNode(n.getValue());
|
||||
var name = node.getName();
|
||||
if (name == "aircraft" or name == "multiplayer")
|
||||
layer.onRemoved(TrafficModel.new(node));
|
||||
}
|
||||
));
|
||||
layer.searcher._equals = func(l,r) l.equals(r);
|
||||
return m;
|
||||
};
|
||||
var del = func() {
|
||||
#print(name~".lcontroller.del()");
|
||||
foreach (var l; me.listeners)
|
||||
removelistener(l);
|
||||
};
|
||||
|
||||
var TrafficModel = {
|
||||
new: func(node, id=nil, layer=nil) {
|
||||
if (id == nil) id = node.getValue("id");
|
||||
var m = {
|
||||
# Note: because this inherits from props.Node, Symbol.Controller.equals
|
||||
# will call l.equals(r) -- the one defined below
|
||||
parents: [TrafficModel, geo.Coord, node],
|
||||
id: id,
|
||||
node: node,
|
||||
pos: node.getNode("position"),
|
||||
};
|
||||
if (m.pos == nil)
|
||||
m.latlon = func [nil,nil,nil];
|
||||
return m;
|
||||
},
|
||||
equals: func(other) other.id == me.id,
|
||||
latlon: func() {
|
||||
return [
|
||||
me.pos.getValue("latitude-deg"),
|
||||
me.pos.getValue("longitude-deg"),
|
||||
me.pos.getValue("altitude-ft")
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
var searchCmd = func {
|
||||
# TODO: this would be a good candidate for splitting across frames
|
||||
#print("Doing query: "~name);
|
||||
|
||||
var result = [];
|
||||
var pos = geo.Coord.new(); # FIXME: all of these should be instance variables
|
||||
var myPosition = geo.Coord.new();
|
||||
# FIXME: need a Map Controller for this, and all query_range's/get_position's
|
||||
var myPositionVec = me.get_position();
|
||||
myPosition.set_latlon( myPositionVec[0], myPositionVec[1]);
|
||||
var max_dist_m = me.query_range()*NM2M;
|
||||
|
||||
# AI and Multiplayer traffic
|
||||
foreach (var traffic; [model_root.getChildren("aircraft"), model_root.getChildren("multiplayer")])
|
||||
foreach(var t; traffic) {
|
||||
pos.set_latlon(t.getValue("position/latitude-deg"),
|
||||
t.getValue("position/longitude-deg"));
|
||||
|
||||
if (pos.distance_to( myPosition ) <= max_dist_m )
|
||||
append(result, TrafficModel.new(t, nil, me.layer));
|
||||
}
|
||||
|
||||
#debug.dump(result);
|
||||
#return [];
|
||||
return result;
|
||||
};
|
||||
|
12
Nasal/canvas/map/TFC.scontroller
Normal file
12
Nasal/canvas/map/TFC.scontroller
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Class things:
|
||||
var name = 'TFC';
|
||||
var parents = [Symbol.Controller];
|
||||
var __self__ = caller(0)[0];
|
||||
Symbol.Controller.add(name, __self__);
|
||||
Symbol.registry[name].df_controller = __self__;
|
||||
var new = func(model) ; # this controller doesn't need an instance
|
||||
# XXX: this is more model-ish than controller-ish
|
||||
var get_threat_lvl = func(model) model.getValue("tcas/threat-level");
|
||||
var get_vspd = func(model) (model.getValue("velocities/vertical-speed-fps") or 0)*60;
|
||||
var get_alt_diff = func(model) (model.getValue("position/altitude-ft") or 0) - (getprop("/position/altitude-ft") or 0);
|
||||
|
91
Nasal/canvas/map/TFC.symbol
Normal file
91
Nasal/canvas/map/TFC.symbol
Normal file
|
@ -0,0 +1,91 @@
|
|||
# Class things:
|
||||
var name = 'TFC';
|
||||
var parents = [DotSym];
|
||||
var __self__ = caller(0)[0];
|
||||
DotSym.makeinstance( name, __self__ );
|
||||
|
||||
var element_type = "group"; # we want a group, becomes "me.element"
|
||||
var text_tcas = nil;
|
||||
var icon_tcas = nil;
|
||||
var arrow_tcas = [nil,nil];
|
||||
var arrow_type = nil;
|
||||
|
||||
var draw_tcas_arrow = nil;
|
||||
|
||||
var draw = func {
|
||||
if (me.draw_tcas_arrow == nil)
|
||||
me.draw_tcas_arrow = [
|
||||
draw_tcas_arrow_above_500,
|
||||
draw_tcas_arrow_below_500
|
||||
];
|
||||
#var callsign = me.model.getNode("callsign").getValue();
|
||||
# print("Drawing traffic for:", callsign );
|
||||
var threatLvl = me.controller.get_threat_lvl(me.model);
|
||||
var vspeed = me.controller.get_vspd(me.model);
|
||||
var altDiff = me.controller.get_alt_diff(me.model);
|
||||
# Init
|
||||
if (me.text_tcas == nil) {
|
||||
me.text_tcas = me.element.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");
|
||||
me.icon_tcas = me.element.createChild("path")
|
||||
.setStrokeLineWidth(3);
|
||||
}
|
||||
# Update
|
||||
if (altDiff > 0)
|
||||
me.text_tcas.setTranslation(0,-40);
|
||||
else
|
||||
me.text_tcas.setTranslation(0,40);
|
||||
var arrow_type = (vspeed >= 500);
|
||||
if (arrow_type != me.arrow_type) {
|
||||
(old_type, me.arrow_type) = (me.arrow_type, arrow_type);
|
||||
if (old_type != nil and me.arrow_tcas[old_type] != nil) me.arrow_tcas[old_type].hide();
|
||||
if (me.arrow_tcas[arrow_type] == nil)
|
||||
me.arrow_tcas[arrow_type] = me.draw_tcas_arrow[arrow_type](me.element);
|
||||
me.arrow_tcas[arrow_type].show();
|
||||
}
|
||||
## TODO: threat level symbols should also be moved to *.draw files
|
||||
if (threatLvl == 3) {
|
||||
# resolution advisory
|
||||
me.icon_tcas.moveTo(-17,-17)
|
||||
.horiz(34)
|
||||
.vert(34)
|
||||
.horiz(-34)
|
||||
.close()
|
||||
.setColor(1,0,0)
|
||||
.setColorFill(1,0,0);
|
||||
me.text_tcas.setColor(1,0,0);
|
||||
me.arrow_tcas.setColor(1,0,0);
|
||||
} elsif (threatLvl == 2) {
|
||||
# traffic advisory
|
||||
me.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);
|
||||
me.text_tcas.setColor(1,0.5,0);
|
||||
me.arrow_tcas.setColor(1,0.5,0);
|
||||
} elsif (threatLvl == 1) {
|
||||
# proximate traffic
|
||||
me.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
|
||||
me.icon_tcas.moveTo(-10,0)
|
||||
.lineTo(0,-17)
|
||||
.lineTo(10,0)
|
||||
.lineTo(0,17)
|
||||
.close()
|
||||
.setColor(1,1,1);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
# Class things:
|
||||
var name = 'VOR';
|
||||
var parents = [Symbol.Controller];
|
||||
var __self__ = caller(0)[0];
|
||||
Symbol.Controller.add("VOR", __self__);
|
||||
Symbol.registry["VOR"].df_controller = __self__;
|
||||
Symbol.Controller.add(name, __self__);
|
||||
Symbol.registry[name].df_controller = __self__;
|
||||
var new = func(model) ; # this controller doesn't need an instance
|
||||
var LayerController = SymbolLayer.Controller.registry["VOR"];
|
||||
var LayerController = SymbolLayer.Controller.registry[name];
|
||||
var isActive = func(model) LayerController.a_instance.isActive(model);
|
||||
var query_range = func()
|
||||
die("VOR.scontroller.query_range /MUST/ be provided by implementation");
|
||||
die(name~".scontroller.query_range /MUST/ be provided by implementation");
|
||||
|
||||
|
|
|
@ -14,22 +14,21 @@ var new = func(layer) {
|
|||
layer: layer,
|
||||
listeners: [],
|
||||
query_range_nm: 25,
|
||||
query_type:'vor',
|
||||
};
|
||||
return m;
|
||||
};
|
||||
var del = func() {
|
||||
#print("VOR.lcontroller.del()");
|
||||
#print(name~"VOR.lcontroller.del()");
|
||||
foreach (var l; me.listeners)
|
||||
removelistener(l);
|
||||
};
|
||||
|
||||
var searchCmd = func {
|
||||
#print("Running query: WPT");
|
||||
#print("Running query: "~name);
|
||||
|
||||
var fp = flightplan();
|
||||
var fpSize = fp.getPlanSize();
|
||||
var result = [];
|
||||
var result = [];
|
||||
for (var i = 1; i <fpSize; i+=1)
|
||||
append(result, fp.getWP(i).path()[0] );
|
||||
|
||||
|
|
|
@ -132,12 +132,11 @@ var NDStyles = {
|
|||
{ name:'DME', isMapStructure:1, update_on:['toggle_range','toggle_stations'],
|
||||
# FIXME: this is a really ugly place for controller code
|
||||
predicate: func(nd, layer) {
|
||||
var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40);
|
||||
# print("Running vor layer predicate");
|
||||
# toggle visibility here
|
||||
layer.group.setVisible( nd.get_switch('toggle_stations') );
|
||||
if (nd.rangeNm() <= 40 and
|
||||
nd.get_switch('toggle_stations') and
|
||||
nd.get_switch('toggle_display_mode') == "MAP") {
|
||||
layer.group.setVisible( visible );
|
||||
if (visible) {
|
||||
#print("Updating MapStructure ND layer: DME");
|
||||
# (Hopefully) smart update
|
||||
layer.update();
|
||||
|
@ -145,7 +144,7 @@ var NDStyles = {
|
|||
}, # end of layer update predicate
|
||||
}, # end of DME layer
|
||||
|
||||
{ name:'mp-traffic', update_on:['toggle_range','toggle_traffic'],
|
||||
{ name:'mp-traffic', disabled:1, update_on:['toggle_range','toggle_traffic'],
|
||||
predicate: func(nd, layer) {
|
||||
var visible = nd.get_switch('toggle_traffic');
|
||||
layer._view.setVisible( visible );
|
||||
|
@ -154,6 +153,16 @@ var NDStyles = {
|
|||
}
|
||||
}, # end of layer update predicate
|
||||
}, # end of traffic layer
|
||||
{ name:'TFC', isMapStructure:1, update_on:['toggle_range','toggle_traffic'],
|
||||
predicate: func(nd, layer) {
|
||||
var visible = nd.get_switch('toggle_traffic');
|
||||
layer.group.setVisible( visible );
|
||||
if (visible) {
|
||||
#print("Updating MapStructure ND layer: TFC");
|
||||
layer.update();
|
||||
}
|
||||
}, # end of layer update predicate
|
||||
}, # end of traffic layer
|
||||
|
||||
{ name:'runway-nd', update_on:['toggle_range','toggle_display_mode'],
|
||||
predicate: func(nd, layer) {
|
||||
|
@ -560,6 +569,7 @@ var NavDisplay = {
|
|||
}
|
||||
|
||||
var get_current_position = func {
|
||||
delete(caller(0)[0], "me"); # remove local me, inherit outer one
|
||||
return [
|
||||
me.aircraft_source.get_lat(), me.aircraft_source.get_lon()
|
||||
];
|
||||
|
@ -581,6 +591,8 @@ var NavDisplay = {
|
|||
canvas.Symbol.Controller.get("VOR").query_range = controller.query_range;
|
||||
canvas.Symbol.Controller.get("VOR").get_tuned_course = controller.get_tuned_course;
|
||||
canvas.Symbol.Controller.get("DME").is_tuned = controller.is_tuned;
|
||||
canvas.SymbolLayer.Controller.get("TFC").query_range = controller.query_range;
|
||||
canvas.SymbolLayer.Controller.get("TFC").get_position = controller.get_position;
|
||||
|
||||
###
|
||||
# set up various layers, controlled via callbacks in the controller hash
|
||||
|
|
|
@ -341,17 +341,45 @@ var PositionedSearch = {
|
|||
};
|
||||
},
|
||||
_equals: func(a,b) {
|
||||
if (a == nil or b == nil) return 0;
|
||||
return (a == b or a.id == b.id);
|
||||
return a == b; # positioned objects are created once
|
||||
#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 (removed[i] != nil and added[j] != nil and 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;
|
||||
var old = me.result~[]; #copyvec
|
||||
me.result = call(searchCmd, nil, me.obj);
|
||||
positioned.diff( old,
|
||||
me.result,
|
||||
func call(me.onAdded, arg, me.obj),
|
||||
func call(me.onRemoved, arg, me.obj) );
|
||||
if (me._equals == PositionedSearch._equals) {
|
||||
# Optimized search using C code
|
||||
var old = me.result~[]; #copyvec
|
||||
me.result = call(searchCmd, nil, me.obj);
|
||||
positioned.diff( old,
|
||||
me.result,
|
||||
func call(me.onAdded, arg, me.obj),
|
||||
func call(me.onRemoved, arg, me.obj) );
|
||||
} else {
|
||||
(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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue