diff --git a/A320-main.xml b/A320-main.xml
index 1a4ed5c8..97c14b71 100644
--- a/A320-main.xml
+++ b/A320-main.xml
@@ -4694,6 +4694,11 @@
Aircraft/A320-family/Nasal/MCDU/DATA2.nas
Aircraft/A320-family/Nasal/MCDU/STATUS.nas
+
+
+ Aircraft/A320-family/Nasal/Displays/projection.nas
+ Aircraft/A320-family/Nasal/Displays/traffic.nas
+
Aircraft/A320-family/Models/Instruments/PFD/PFD.nas
diff --git a/Models/Instruments/ND/canvas/ND.nas b/Models/Instruments/ND/canvas/ND.nas
index 809943de..d1e73e51 100644
--- a/Models/Instruments/ND/canvas/ND.nas
+++ b/Models/Instruments/ND/canvas/ND.nas
@@ -338,6 +338,41 @@ setlistener("sim/signals/fdm-initialized", func {
ND_2 = canvas_ND_2.new(group_nd2);
ND_2_test = canvas_ND_2_test.new(group_nd2_test, "Aircraft/A320-family/Models/Instruments/Common/res/du-test.svg");
+ setlistener("/instrumentation/efis[0]/inputs/range-nm", func() {
+ canvas_nd.ND_1.NDCpt.trafficLayer.camera.range = getprop("/instrumentation/efis[0]/inputs/range-nm");
+ }, 1, 0);
+
+ setlistener("/instrumentation/efis[1]/inputs/range-nm", func() {
+ canvas_nd.ND_2.NDFo.trafficLayer.camera.range = getprop("/instrumentation/efis[1]/inputs/range-nm");
+ }, 1, 0);
+
+ setlistener("/instrumentation/efis[0]/inputs/nd-centered", func() {
+ canvas_nd.ND_1.NDCpt.trafficLayer.camera.screenRange = getprop("/instrumentation/efis[0]/inputs/nd-centered") ? 436.8545 : 710;
+ canvas_nd.ND_1.NDCpt.trafficLayer.camera.screenCY = getprop("/instrumentation/efis[0]/inputs/nd-centered") ? 512 : 850;
+ }, 1, 0);
+
+ setlistener("/instrumentation/efis[1]/inputs/nd-centered", func() {
+ canvas_nd.ND_2.NDFo.trafficLayer.camera.screenRange = getprop("/instrumentation/efis[1]/inputs/nd-centered") ? 436.8545 : 710;
+ canvas_nd.ND_2.NDFo.trafficLayer.camera.screenCY = getprop("/instrumentation/efis[1]/inputs/nd-centered") ? 512 : 850;
+ }, 1, 0);
+
+ setlistener("/instrumentation/tcas/inputs/mode", func() {
+ if (getprop("/instrumentation/efis[1]/nd/canvas-display-mode") != "PLAN") {
+ canvas_nd.ND_1.NDCpt.trafficGroup.setVisible(pts.Instrumentation.TCAS.Inputs.mode.getValue() >= 2 ? 1 : 0);
+ }
+ if (getprop("/instrumentation/efis[1]/nd/canvas-display-mode") != "PLAN") {
+ canvas_nd.ND_2.NDFo.trafficGroup.setVisible(pts.Instrumentation.TCAS.Inputs.mode.getValue() >= 2 ? 1 : 0);
+ }
+ }, 1, 0);
+
+ setlistener("/instrumentation/efis[0]/nd/canvas-display-mode", func() {
+ canvas_nd.ND_1.NDCpt.trafficGroup.setVisible(getprop("/instrumentation/efis[0]/nd/canvas-display-mode") == "PLAN" ? 0 : 1);
+ }, 1, 0);
+
+ setlistener("/instrumentation/efis[1]/nd/canvas-display-mode", func() {
+ canvas_nd.ND_2.NDFo.trafficGroup.setVisible(getprop("/instrumentation/efis[1]/nd/canvas-display-mode") == "PLAN" ? 0 : 1);
+ }, 1, 0);
+
nd_update.start();
if (getprop("systems/acconfig/options/nd-rate") > 1) {
rateApply();
diff --git a/Models/Instruments/ND/canvas/framework/navdisplay.nas b/Models/Instruments/ND/canvas/framework/navdisplay.nas
index 534f4e5a..7778f57c 100644
--- a/Models/Instruments/ND/canvas/framework/navdisplay.nas
+++ b/Models/Instruments/ND/canvas/framework/navdisplay.nas
@@ -234,6 +234,17 @@ canvas.NavDisplay.newMFD = func(canvas_group, parent=nil, nd_options=nil, update
event_handler();
} # foreach layer
+ me.mapCamera = traffic.Camera.new({
+ range: 20,
+ screenRange: 436.8545,
+ screenCX: 512,
+ screenCY: 512,
+ });
+ me.trafficGroup = me.nd.createChild("group");
+ me.trafficLayer = traffic.TrafficLayer.new(me.mapCamera, me.trafficGroup);
+ me.trafficLayer.start();
+ me.trafficGroup.set("z-index", -1);
+
#print("navdisplay.mfd:ND layer setup completed");
# TODO: move this to RTE.lcontroller ?
@@ -391,6 +402,13 @@ canvas.NavDisplay.update = func() # FIXME: This stuff is still too aircraft spec
else
me.map.setTranslation(512,824);
}
+ me.mapCamera.repositon(geo.aircraft_position(), me.aircraft_source.get_hdg_tru());
+ me.pos = props.globals.getNode("position");
+ me.trafficLayer.setRefAlt(me.pos.getValue("altitude-ft"));
+ if (me.trafficGroup.getVisible()) {
+ me.trafficLayer.update();
+ me.trafficLayer.redraw();
+ }
var vor1_path = "/instrumentation/nav[2]";
var vor2_path = "/instrumentation/nav[3]";
var dme1_path = "/instrumentation/dme[2]";
diff --git a/Nasal/Displays/projection.nas b/Nasal/Displays/projection.nas
new file mode 100644
index 00000000..722e2771
--- /dev/null
+++ b/Nasal/Displays/projection.nas
@@ -0,0 +1,41 @@
+# Projection-related helper functions for the MFD maps
+
+var Camera = {
+ new: func(options) {
+ var m = {
+ parents: [Camera],
+
+ camGeo: options['camGeo'] or geo.aircraft_position(),
+ camHdg: options['camHdg'] or 0,
+ range: options['range'] or 10.0,
+ screenRange: options['screenRange'] or 256.0,
+ screenCX: options['screenCX'] or options['screenRange'] or 256.0,
+ screenCY: options['screenCY'] or options['screenRange'] or 256.0,
+ };
+ return m;
+ },
+
+ setRange: func(range) {
+ me.range = range;
+ },
+
+ repositon: func(geo, hdg) {
+ me.camGeo = geo;
+ me.camHdg = hdg;
+ },
+
+ project: func(targetGeo) {
+ var dist = me.camGeo.distance_to(targetGeo) * M2NM;
+ var bearing = me.camGeo.course_to(targetGeo) - me.camHdg;
+ return me.projectDistBearing(dist, bearing);
+ },
+
+ projectDistBearing: func(dist, bearing) {
+ var bearingRad = bearing * D2R;
+ var tx = math.sin(bearingRad) * dist;
+ var ty = -math.cos(bearingRad) * dist;
+ var x = tx * me.screenRange / me.range + me.screenCX;
+ var y = ty * me.screenRange / me.range + me.screenCY;
+ return [x, y];
+ },
+};
diff --git a/Nasal/Displays/traffic.nas b/Nasal/Displays/traffic.nas
new file mode 100644
index 00000000..da3df091
--- /dev/null
+++ b/Nasal/Displays/traffic.nas
@@ -0,0 +1,279 @@
+# Traffic layer
+#
+
+var colorByLevel = {
+ # 0: other
+ 0: [0.8,0.8,0.8],
+ # 1: proximity
+ 1: [0.8,0.8,0.8],
+ # 2: traffic advisory (TA)
+ 2: [1,0.75,0],
+ # 3: resolution advisory (RA)
+ 3: [1,0,0],
+};
+
+var doFill = {
+ 0: 0,
+ 1: 1,
+ 2: 1,
+ 3: 1,
+};
+
+var colorDefault = [0.8,0.8,0.8];
+
+var drawBlip = func(elem, threatLvl) {
+ if (threatLvl == 3) {
+ # resolution advisory
+ elem.reset()
+ .moveTo(-17,-17)
+ .horiz(34)
+ .vert(34)
+ .horiz(-34)
+ .close();
+ }
+ elsif (threatLvl == 2) {
+ # traffic advisory
+ elem.reset()
+ .moveTo(-17,0)
+ .arcSmallCW(17,17,0,34,0)
+ .arcSmallCW(17,17,0,-34,0);
+ }
+ elsif (threatLvl == 1) {
+ # proximate traffic
+ elem.reset()
+ .moveTo(-14,0)
+ .lineTo(0,-17)
+ .lineTo(14,0)
+ .lineTo(0,17)
+ .close();
+ }
+ else {
+ # other traffic
+ elem.reset()
+ .moveTo(-14,0)
+ .lineTo(0,-17)
+ .lineTo(14,0)
+ .lineTo(0,17)
+ .close();
+ }
+};
+
+
+var TrafficLayer = {
+ new: func(camera, group) {
+ var m = {
+ parents: [TrafficLayer],
+ camera: camera,
+ refAlt: 0,
+ group: group,
+ items: {},
+ updateKeys: [],
+ addListener: nil,
+ delListener: nil,
+ };
+ return m;
+ },
+
+ makeElems: func () {
+ if (me.group == nil) return nil;
+ var elems = {};
+ elems['master'] = me.group.createChild('group');
+ elems['blip'] = elems.master.createChild('path')
+ .setStrokeLineWidth(0);
+ elems['text'] = elems.master.createChild('text')
+ .setDrawMode(canvas.Text.TEXT)
+ .setText(sprintf("0"))
+ .setFont("LiberationFonts/LiberationSans-Regular.ttf")
+ .setColor(1,1,1)
+ .setFontSize(20)
+ .setAlignment("center-center");
+ elems['master'].hide();
+ elems['arrowUp'] = elems.master.createChild("text")
+ .setDrawMode(canvas.Text.TEXT)
+ .setText(sprintf("↑"))
+ .setFont("LiberationFonts/LiberationSans-Regular.ttf")
+ .setColor(1,1,1)
+ .setFontSize(40)
+ .setTranslation(16, 0)
+ .setAlignment("left-center");
+ elems['arrowDown'] = elems.master.createChild("text")
+ .setDrawMode(canvas.Text.TEXT)
+ .setText(sprintf("↓"))
+ .setFont("LiberationFonts/LiberationSans-Regular.ttf")
+ .setColor(1,1,1)
+ .setFontSize(40)
+ .setTranslation(16, 0)
+ .setAlignment("left-center");
+ return elems;
+ },
+
+ start: func() {
+ me.stop();
+ var self = me;
+ me.addListener = setlistener('/ai/models/model-added', func(changed, listen, mode, is_child) {
+ var path = changed.getValue();
+ if (path == nil) return;
+ #printf("ADD: %s", path);
+ var masterProp = props.globals.getNode(path);
+ var prop = {
+ 'master': masterProp,
+ };
+ if (me.items[path] == nil) {
+ me.items[path] = {
+ prop: prop,
+ elems: me.makeElems(),
+ data: {'threatLevel': -2},
+ };
+ }
+ else {
+ me.items[path].prop = prop;
+ me.items[path].data = {'threatLevel': -2};
+ }
+ }, 1, 1);
+ me.delListener = setlistener('/ai/models/model-removed', func(changed, listen, mode, is_child) {
+ var path = changed.getValue();
+ if (path == nil) return;
+ #printf("DEL: %s", path);
+ if (me.items[path] == nil) return;
+ if (me.items[path] != nil) {
+ me.items[path].prop = nil;
+ me.items[path].elems.master.hide();
+ me.items[path].data = {};
+ }
+ }, 1, 1);
+ },
+
+ stop: func() {
+ if (me.addListener != nil) {
+ removelistener(me.addListener);
+ me.addListener = nil;
+ }
+ if (me.delListener != nil) {
+ removelistener(me.delListener);
+ me.delListener = nil;
+ }
+ me.items = {};
+ if (me.group != nil) {
+ me.group.removeAllChildren();
+ }
+ },
+
+ update: func() {
+ if (size(me.updateKeys) == 0) {
+ me.updateKeys = keys(me.items);
+ }
+ var path = pop(me.updateKeys);
+ foreach (var path; keys(me.items)) {
+ me.updateItem(path);
+ }
+ },
+
+ redraw: func() {
+ foreach (var path; keys(me.items)) {
+ me.redrawItem(me.items[path]);
+ }
+ },
+
+ setRefAlt: func(alt) {
+ me.refAlt = alt;
+ },
+
+ updateItem: func(path) {
+ var item = me.items[path];
+ if (item == nil) return;
+ if (item.prop == nil) {
+ if (item.elems != nil) {
+ item.elems.master.hide();
+ }
+ return;
+ }
+
+ if (item.prop['lat'] == nil) {
+ item.prop['lat'] = item.prop.master.getNode('position/latitude-deg');
+ }
+ if (item.prop['lon'] == nil) {
+ item.prop['lon'] = item.prop.master.getNode('position/longitude-deg');
+ }
+ if (item.prop['alt'] == nil) {
+ item.prop['alt'] = item.prop.master.getNode('position/altitude-ft');
+ }
+ if (item.prop['threatLevel'] == nil) {
+ item.prop['threatLevel'] = item.prop.master.getNode('tcas/threat-level');
+ }
+ if (item.prop['callsign'] == nil) {
+ item.prop['callsign'] = item.prop.master.getNode('callsign');
+ }
+ if (item.prop['vspeed'] == nil) {
+ item.prop['vspeed'] = item.prop.master.getNode('velocities/vertical-speed-fps');
+ }
+
+ # this item has a prop associated with it
+ if (item.elems == nil) {
+ item.elems = me.makeElems();
+ }
+ var oldThreatLevel = item.data['threatLevel'];
+ foreach (var k; ['lat', 'lon', 'alt', 'threatLevel', 'callsign', 'vspeed']) {
+ if (item.prop[k] != nil) {
+ item.data[k] = item.prop[k].getValue();
+ }
+ }
+ if (oldThreatLevel != item.data['threatLevel']) {
+ item.data['threatLevelDirty'] = 1;
+ }
+ },
+
+ redrawItem: func (item) {
+ #debug.dump("REDRAW ", item.data);
+ var lat = item.data['lat'];
+ var lon = item.data['lon'];
+ var alt = item.data['alt'];
+ var vspeed = item.data['vspeed'];
+ var threatLevelDirty = item.data['threatLevelDirty'];
+ if (lat != nil and lon != nil and vspeed != nil) {
+ var coords = geo.Coord.new();
+ coords.set_latlon(lat, lon);
+ var (x, y) = me.camera.project(coords);
+ item.elems.master.setTranslation(x, y);
+ #printf("%f %f", x, y);
+ if (threatLevelDirty) {
+ #printf('%s THREAT LVL: %i', item.data['callsign'] or '???', item.data['threatLevel']);
+ var threatLevel = item.data['threatLevel'];
+ #debug.dump(item.data, threatLevel);
+ drawBlip(item.elems.blip, threatLevel);
+ var rgb = colorByLevel[threatLevel];
+ if (rgb == nil) rgb = colorDefault;
+ var color = canvas._getColor(rgb);
+ var (r, g, b) = rgb;
+ item.elems.blip.setColorFill(r, g, b);
+ item.elems.text.setColor(r, g, b);
+ item.elems.arrowUp.setColor(r, g, b);
+ item.elems.arrowDown.setColor(r, g, b);
+ item.elems.master.set('z-index', threatLevel + 2);
+ item.data['threatLevelDirty'] = 0;
+ }
+
+ item.elems.arrowUp.setVisible(vspeed * 60 > 500);
+ item.elems.arrowDown.setVisible(vspeed * 60 < -500);
+
+ var altDiff100 = ((item.data['alt'] or me.refAlt) - me.refAlt) / 100;
+ item.elems.text.setVisible(math.abs(altDiff100) > 0.5);
+ item.elems.text.setText(sprintf("%+02.0f", altDiff100));
+ if (altDiff100 < 0) {
+ item.elems.text.setTranslation(0, 30);
+ item.elems.arrowUp.setTranslation(16, 30);
+ item.elems.arrowDown.setTranslation(16, 30);
+ }
+ else {
+ item.elems.text.setTranslation(0, -30);
+ item.elems.arrowUp.setTranslation(16, -30);
+ item.elems.arrowDown.setTranslation(16, -30);
+ }
+
+ item.elems.master.show();
+ }
+ else {
+ item.elems.master.hide();
+ }
+ },
+
+};