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