diff --git a/Nasal/canvas/MapStructure.nas b/Nasal/canvas/MapStructure.nas index 5474e9145..2ec29eb80 100644 --- a/Nasal/canvas/MapStructure.nas +++ b/Nasal/canvas/MapStructure.nas @@ -1,3 +1,123 @@ +var dump_obj = func(m) { + var h = {}; + foreach (var k; keys(m)) + if (k != "parents") + h[k] = m[k]; + debug.dump(h); +}; + +## +# must be either of: +# 1) draw* callback, 2) SVG filename, 3) Drawable class (with styling/LOD support) +var SymbolDrawable = { + new: func() { + }, +}; + +## wrapper for each element +## i.e. keeps the canvas and texture map coordinates +var CachedElement = { + new: func(canvas_path, name, source, offset) { + var m = {parents:[CachedElement] }; + m.canvas_src = canvas_path; + m.name = name; + m.source = source; + m.offset = offset; + return m; + }, # new() + render: func(group) { + # create a raster image child in the render target/group + return + group.createChild("image", me.name) + .setFile( me.canvas_src ) + # TODO: fix .setSourceRect() to accept a single vector for coordinates ... + .setSourceRect(left:me.source[0],top:me.source[1],right:me.source[2],bottom:me.source[3] , normalized:0) + .setTranslation(me.offset); # FIXME: make sure this stays like this and isn't overridden + }, # render() +}; # of CachedElement + +var SymbolCache = { + # We can draw symbols either with left/top, centered, + # or right/bottom alignment. Specify two in a vector + # to mix and match, e.g. left/centered would be + # [SymbolCache.DRAW_LEFT_TOP,SymbolCache.DRAW_CENTERED] + DRAW_LEFT_TOP: 0.0, + DRAW_CENTERED: 0.5, + DRAW_RIGHT_BOTTOM: 1.0, + new: func(dim...) { + var m = { parents:[SymbolCache] }; + # to keep track of the next free caching spot (in px) + m.next_free = [0, 0]; + # to store each type of symbol + m.dict = {}; + if (size(dim) == 1 and typeof(dim[0]) == 'vector') + dim = dim[0]; + # Two sizes: canvas and symbol + if (size(dim) == 2) { + var canvas_x = var canvas_y = dim[0]; + var image_x = var image_y = dim[1]; + # Two widths (canvas and symbol) and then height/width ratio + } else if (size(dim) == 3) { + var (canvas_x,image_x,ratio) = dim; + var canvas_y = canvas_x * ratio; + var image_y = image_x * ratio; + # Explicit canvas and symbol widths/heights + } else if (size(dim) == 4) { + var (canvas_x,canvas_y,image_x,image_y) = dim; + } + m.canvas_sz = [canvas_x, canvas_y]; + m.image_sz = [image_x, image_y]; + + # allocate a canvas + m.canvas_texture = canvas.new( { + "name": "SymbolCache"~canvas_x~'x'~canvas_y, + "size": m.canvas_sz, + "view": m.canvas_sz, + "mipmapping": 1 + }); + + # add a placement + m.canvas_texture.addPlacement( {"type": "ref"} ); + + return m; + }, + add: func(name, callback, draw_mode=0) { + if (typeof(draw_mode) == 'scalar') + var draw_mode0 = var draw_mode1 = draw_mode; + else var (draw_mode0,draw_mode1) = draw_mode; + # get canvas texture that we use as cache + # get next free spot in texture (column/row) + # run the draw callback and render into a group + var gr = me.canvas_texture.createGroup(); + gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0, + me.next_free[1] + me.image_sz[1]*draw_mode1); + #settimer(func debug.dump ( gr.getTransformedBounds() ), 0); # XXX: these are only updated when rendered + #debug.dump ( gr.getTransformedBounds() ); + gr.update(); # apparently this doesn't result in sane output from .getTransformedBounds() either + #debug.dump ( gr.getTransformedBounds() ); + # draw the symbol + callback(gr); + # get the bounding box, i.e. coordinates for texture map, or use the .setTranslation() params + var coords = me.next_free~me.next_free; + foreach (var i; [0,1]) + coords[i+2] += me.image_sz[i]; + # get the offset we used to position correctly in the bounds of the canvas + var offset = [me.image_sz[0]*draw_mode0, me.image_sz[1]*draw_mode1]; + # store texture map coordinates in lookup map using the name as identifier + me.dict[name] = CachedElement.new(me.canvas_texture.getPath(), name, coords, offset ); + # update next free position in cache (column/row) + me.next_free[0] += me.image_sz[0]; + if (me.next_free[0] >= me.canvas_sz[0]) + { me.next_free[0] = 0; me.next_free[1] += me.image_sz[1] } + if (me.next_free[1] >= me.canvas_sz[1]) + die("SymbolCache: ran out of space after adding '"~name~"'"); + }, # add() + get: func(name) { + if(!contains(me.dict,name)) die("No SymbolCache entry for key:"~ name); + return me.dict[name]; + }, # get() +}; + var Symbol = { # Static/singleton: registry: {}, @@ -29,7 +149,7 @@ Symbol.Controller = { # Static/singleton: registry: {}, add: func(type, class) - registry[type] = class, + me.registry[type] = class, get: func(type) if ((var class = me.registry[type]) == nil) die("unknown type '"~type~"'"); @@ -58,13 +178,17 @@ var getpos_fromghost = func(positioned_g) # (geo.Coord and positioned ghost currently) Symbol.Controller.getpos = func(obj) { if (typeof(obj) == 'ghost') - if (ghosttype(obj) == 'positioned' or ghosttype(obj) == 'Navaid') + if (ghosttype(obj) == 'positioned' or ghosttype(obj) == 'Navaid' or ghosttype(obj)=='Fix' or ghosttype(obj)=='flightplan-leg') return getpos_fromghost(obj); else die("bad ghost of type '"~ghosttype(obj)~"'"); if (typeof(obj) == 'hash') if (isa(obj, geo.Coord)) return obj.latlon(); + if (contains(obj,'lat') and contains(obj,'lon')) + return [obj.lat, obj.lon]; + + debug.dump(obj); die("no suitable getpos() found! Of type: "~typeof(obj)); }; @@ -82,38 +206,17 @@ var DotSym = { 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]; + if (!isa(hash, DotSym)) + die("OOP error"); + #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) + #); 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 @@ -132,6 +235,10 @@ var DotSym = { ), }; if (m.controller != nil) { + #print("Creating controller"); + temp = m.controller.new(m.model,m); + if (temp != nil) + m.controller = temp; #print("Initializing controller"); m.controller.init(model); } @@ -281,93 +388,117 @@ SymbolLayer.Controller = { 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), +var AnimatedLayer = { }; -####### 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 "]); +var CompassLayer = { +}; + +var AltitudeArcLayer = { +}; + +load_MapStructure = 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; } - 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; + }; + + var load_deps = func(name) { + load(FG_ROOT~"/Nasal/canvas/map/"~name~".lcontroller", name); + load(FG_ROOT~"/Nasal/canvas/map/"~name~".symbol", name); + load(FG_ROOT~"/Nasal/canvas/map/"~name~".scontroller", name); } - #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); + foreach( var name; ['VOR','FIX','NDB','DME','WPT'] ) + load_deps( name ); + load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", name); - 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); -}, 0); # end ugly module init timer hack + ### + # set up a cache for 32x32 symbols + var SymbolCache32x32 = SymbolCache.new(1024,32); + var drawVOR = func(color, width=3) return func(group) { + # print("drawing vor"); + var bbox = group.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(width) + .setColor( color ); + # debug.dump( bbox.getBoundingBox() ); + }; + + var cachedVOR1 = SymbolCache32x32.add( "VOR-BLUE", drawVOR( color:[0, 0.6, 0.85], width:3), SymbolCache.DRAW_CENTERED ); + var cachedVOR2 = SymbolCache32x32.add( "VOR-RED" , drawVOR( color:[1.0, 0, 0], width: 3), SymbolCache.DRAW_CENTERED ); + var cachedVOR3 = SymbolCache32x32.add( "VOR-GREEN" , drawVOR( color:[0, 1, 0], width: 3), SymbolCache.DRAW_CENTERED ); + var cachedVOR4 = SymbolCache32x32.add( "VOR-WHITE" , drawVOR( color:[1, 1, 1], width: 3), SymbolCache.DRAW_CENTERED ); + + # STRESS TEST + if (0) { + for(var i=0;i <= 1024/32*4 - 4; i+=1) + SymbolCache32x32.add( "VOR-YELLOW"~i , drawVOR( color:[1, 1, 0], width: 3) ); + + var dlg = canvas.Window.new([640,320],"dialog"); + var my_canvas = dlg.createCanvas().setColorBackground(1,1,1,1); + var root = my_canvas.createGroup(); + + SymbolCache32x32.get(name:"VOR-BLUE").render( group: root ).setGeoPosition(getprop("/position/latitude-deg"),getprop("/position/longitude-deg")); + } + + })(); + #print("finished loading files"); + ####### TEST SYMBOL ####### + + canvas.load_MapStructure = func; + +}; # load_MapStructure + +setlistener("/nasal/canvas/loaded", load_MapStructure); # end ugly module init listener hack diff --git a/Nasal/canvas/api.nas b/Nasal/canvas/api.nas index 8cccc71f8..bd42c061d 100644 --- a/Nasal/canvas/api.nas +++ b/Nasal/canvas/api.nas @@ -462,47 +462,45 @@ var Map = { addLayer: func(factory, type_arg=nil, priority=nil) { if (!contains(me, "layers")) - me.layers = []; + me.layers = {}; + + if(contains(me.layers, type_arg)) + print("addLayer() warning: overwriting existing layer:", type_arg); + + # print("addLayer():", type_arg); # Argument handling if (type_arg != nil) var type = factory.get(type_arg); else var type = factory; + me.layers[type_arg]= type.new(me); if (priority == nil) priority = type.df_priority; - append(me.layers, [type.new(me), priority]); if (priority != nil) - me._sort_priority(); + me.layers[type_arg].setInt("z-index", priority); return me; }, - setPos: func(lat,lon,hdg=nil) + getLayer: func(type_arg) me.layers[type_arg], + setPos: func(lat, lon, hdg=nil, range=nil) { me.set("ref-lat", lat); me.set("ref-lon", lon); if (hdg != nil) me.set("hdg", hdg); - - # me.map.set("range", 100); + if (range != nil) + me.set("range", range); }, # Update each layer on this Map. Called by # me.controller. update: func { - foreach (var l; me.layers) - call(l[0].update, arg, l[0]); + foreach (var l; keys(me.layers)) { + var layer = me.layers[l]; + call(layer.update, arg, layer); + } 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 diff --git a/Nasal/canvas/map.nas b/Nasal/canvas/map.nas index 1b91fe76b..187619913 100644 --- a/Nasal/canvas/map.nas +++ b/Nasal/canvas/map.nas @@ -429,10 +429,16 @@ var files_with = func(ext) { } return results; } -foreach(var ext; var extensions = ['.draw','.model','.layer']) - load_modules(files_with(ext)); +setlistener("/nasal/canvas/loaded", func { + foreach(var ext; var extensions = ['.draw','.model','.layer']) + load_modules(files_with(ext)); + + if (contains(canvas,"load_MapStructure")) + load_MapStructure(); + + # 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' ); +}); -# 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' ); diff --git a/Nasal/canvas/map/DME.lcontroller b/Nasal/canvas/map/DME.lcontroller new file mode 100644 index 000000000..b2d3c714e --- /dev/null +++ b/Nasal/canvas/map/DME.lcontroller @@ -0,0 +1,32 @@ +# Class things: +var name = 'DME'; +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 a_instance = nil; +var new = func(layer) { + var m = { + parents: [__self__], + layer: layer, + listeners: [], + query_range_nm: 25, + query_type:'dme', + }; + __self__.a_instance = m; + return m; +}; +var del = func() { + foreach (var l; me.listeners) + removelistener(l); +}; + +var searchCmd = func { + #print("Running query:", me.query_type); + return positioned.findWithinRange(me.query_range_nm, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch +}; + diff --git a/Nasal/canvas/map/DME.scontroller b/Nasal/canvas/map/DME.scontroller new file mode 100644 index 000000000..0e54d4dc2 --- /dev/null +++ b/Nasal/canvas/map/DME.scontroller @@ -0,0 +1,12 @@ +# Class things: +var name = 'DME'; +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 +var LayerController = SymbolLayer.Controller.registry[ name ]; +var isActive = func(model) LayerController.a_instance.isActive(model); +var is_tuned = func() + die( name~".scontroller.is_tuned /MUST/ be provided by implementation" ); + diff --git a/Nasal/canvas/map/DME.symbol b/Nasal/canvas/map/DME.symbol new file mode 100644 index 000000000..fbf523227 --- /dev/null +++ b/Nasal/canvas/map/DME.symbol @@ -0,0 +1,35 @@ +# Class things: +var name = 'DME'; +var parents = [DotSym]; +var __self__ = caller(0)[0]; +DotSym.makeinstance( name, __self__ ); + +var element_type = "group"; # we want a group, becomes "me.element" +var icon_dme = nil; + +var draw = func { + # Init + if (me.icon_dme == nil) { + me.icon_dme = me.element.createChild("path") + .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); + } + if (me.controller != nil and me.controller.is_tuned(me.model.frequency/100)) + me.icon_dme.setColor(0,1,0); + else + me.icon_dme.setColor(0,0.6,0.85); +}; + diff --git a/Nasal/canvas/map/FIX.lcontroller b/Nasal/canvas/map/FIX.lcontroller new file mode 100644 index 000000000..ed89fb06b --- /dev/null +++ b/Nasal/canvas/map/FIX.lcontroller @@ -0,0 +1,33 @@ +# Class things: +var name = 'FIX'; +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 a_instance = nil; +var new = func(layer) { + var m = { + parents: [__self__], + layer: layer, + listeners: [], + query_range_nm: 25, + query_type:'fix', + }; + __self__.a_instance = m; + return m; +}; +var del = func() { + #print("VOR.lcontroller.del()"); + foreach (var l; me.listeners) + removelistener(l); +}; + +var searchCmd = func { + #print("Running query:", me.query_type); + return positioned.findWithinRange(me.query_range_nm, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch +}; + diff --git a/Nasal/canvas/map/FIX.scontroller b/Nasal/canvas/map/FIX.scontroller new file mode 100644 index 000000000..5726f86ab --- /dev/null +++ b/Nasal/canvas/map/FIX.scontroller @@ -0,0 +1,12 @@ +# Class things: +var name = 'FIX'; +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 +var LayerController = SymbolLayer.Controller.registry[ name ]; +var isActive = func(model) LayerController.a_instance.isActive(model); +var query_range = func() + die( name~".scontroller.query_range /MUST/ be provided by implementation" ); + diff --git a/Nasal/canvas/map/FIX.symbol b/Nasal/canvas/map/FIX.symbol new file mode 100644 index 000000000..c2eee2c11 --- /dev/null +++ b/Nasal/canvas/map/FIX.symbol @@ -0,0 +1,30 @@ +# Class things: +var name = 'FIX'; +var parents = [DotSym]; +var __self__ = caller(0)[0]; +DotSym.makeinstance( name, __self__ ); + +var element_type = "group"; # we want a group, becomes "me.element" +var icon_fix = nil; +var text_fix = nil; + +var draw = func { + if (me.icon_fix != nil) return; + # the fix symbol + me.icon_fix = me.element.createChild("path") + .moveTo(-15,15) + .lineTo(0,-15) + .lineTo(15,15) + .close() + .setStrokeLineWidth(3) + .setColor(0,0.6,0.85) + .setScale(0.5,0.5); # FIXME: do proper LOD handling here - we need to scale according to current texture dimensions vs. original/design dimensions + # the fix label + me.text_fix = me.element.createChild("text") + .setDrawMode( canvas.Text.TEXT ) + .setText(me.model.id) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") + .setFontSize(28) + .setTranslation(5,25); +}; + diff --git a/Nasal/canvas/map/NDB.lcontroller b/Nasal/canvas/map/NDB.lcontroller new file mode 100644 index 000000000..3250fc0f7 --- /dev/null +++ b/Nasal/canvas/map/NDB.lcontroller @@ -0,0 +1,30 @@ +# Class things: +var name = 'NDB'; +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 new = func(layer) { + var m = { + parents: [__self__], + layer: layer, + listeners: [], + query_range_nm: 25, + query_type:'ndb', + }; + return m; +}; +var del = func() { + foreach (var l; me.listeners) + removelistener(l); +}; + +var searchCmd = func { + #print("Running query:", me.query_type); + return positioned.findWithinRange(me.query_range_nm, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch +}; + diff --git a/Nasal/canvas/map/NDB.scontroller b/Nasal/canvas/map/NDB.scontroller new file mode 100644 index 000000000..7d1011db0 --- /dev/null +++ b/Nasal/canvas/map/NDB.scontroller @@ -0,0 +1,12 @@ +# Class things: +var name = 'NDB'; +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 +var LayerController = SymbolLayer.Controller.registry[ name ]; +var isActive = func(model) LayerController.a_instance.isActive(model); +var query_range = func() + die( name~".scontroller.query_range /MUST/ be provided by implementation" ); + diff --git a/Nasal/canvas/map/NDB.symbol b/Nasal/canvas/map/NDB.symbol new file mode 100644 index 000000000..541b4cebb --- /dev/null +++ b/Nasal/canvas/map/NDB.symbol @@ -0,0 +1,18 @@ +# Class things: +var name = 'NDB'; +var parents = [DotSym]; +var __self__ = caller(0)[0]; +DotSym.makeinstance( name, __self__ ); + +var element_type = "group"; # we want a group, becomes "me.element", which we parse a SVG onto +var svg_path = "/gui/dialogs/images/ndb_symbol.svg"; # speaking of path, this is our path to use +var local_svg_path = nil; # track changes in the SVG's path + +var draw = func { + if (me.svg_path == me.local_svg_path) return; + me.element.removeAllChildren(); + me.local_svg_path = me.svg_path; + canvas.parsesvg(me.element, me.svg_path); + me.inited = 1; +}; + diff --git a/Nasal/canvas/map/VOR.lcontroller b/Nasal/canvas/map/VOR.lcontroller index 1f63e4e67..f01f83da9 100644 --- a/Nasal/canvas/map/VOR.lcontroller +++ b/Nasal/canvas/map/VOR.lcontroller @@ -7,6 +7,7 @@ SymbolLayer.add("VOR", { type: "VOR", # Symbol type df_controller: __self__, # controller to use by default -- this one }); +var a_instance = nil; var new = func(layer) { var m = { parents: [__self__], @@ -14,6 +15,7 @@ var new = func(layer) { active_vors: [], navNs: props.globals.getNode("instrumentation").getChildren("nav"), listeners: [], + query_type:'vor', }; setsize(m.active_vors, size(m.navNs)); foreach (var navN; m.navNs) { @@ -24,6 +26,7 @@ var new = func(layer) { } #call(debug.dump, keys(layer)); m.changed_freq(update:0); + __self__.a_instance = m; return m; }; var del = func() { @@ -46,7 +49,7 @@ var changed_freq = func(update=1) { 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 + #print("Running query:", me.query_type); + return positioned.findWithinRange(100, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch }; diff --git a/Nasal/canvas/map/VOR.scontroller b/Nasal/canvas/map/VOR.scontroller index b95bcc4f2..65fe8f408 100644 --- a/Nasal/canvas/map/VOR.scontroller +++ b/Nasal/canvas/map/VOR.scontroller @@ -1,10 +1,11 @@ # Class things: var parents = [Symbol.Controller]; var __self__ = caller(0)[0]; +Symbol.Controller.add("VOR", __self__); 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 LayerController = SymbolLayer.Controller.registry["VOR"]; +var isActive = func(model) LayerController.a_instance.isActive(model); var query_range = func() die("VOR.scontroller.query_range /MUST/ be provided by implementation"); diff --git a/Nasal/canvas/map/VOR.symbol b/Nasal/canvas/map/VOR.symbol index aaa259dcc..faf37c206 100644 --- a/Nasal/canvas/map/VOR.symbol +++ b/Nasal/canvas/map/VOR.symbol @@ -1,52 +1,57 @@ -# Read by the DotSym.readinstance; each variable becomes a derived class's member/method +# Class things: +var name = 'VOR'; +var parents = [DotSym]; +var __self__ = caller(0)[0]; +DotSym.makeinstance( name, __self__ ); 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 icon_vor = nil; 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); + # Init + if (me.icon_vor == nil) { + me.icon_vor = 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); + } + # 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(); + var course = me.controller.get_tuned_course(me.model.frequency/100); + me.radial_vor = me.element.createChild("path") + .moveTo(0,-radius) + .vert(2*radius) + .setStrokeLineWidth(3) + .setStrokeDashArray([15, 5, 15, 5, 15]) + .setColor(0,1,0) + .setRotation(course*D2R); + me.icon_vor.setColor(0,1,0); } - } 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); + me.range_vor.show(); + me.radial_vor.show(); + } elsif (me.range_vor != nil) { + me.range_vor.hide(); + me.radial_vor.hide(); + } }; diff --git a/Nasal/canvas/map/WPT.lcontroller b/Nasal/canvas/map/WPT.lcontroller new file mode 100644 index 000000000..13bcc2f63 --- /dev/null +++ b/Nasal/canvas/map/WPT.lcontroller @@ -0,0 +1,39 @@ +# Class things: +var name = 'WPT'; # for waypoints +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 new = func(layer) { + var m = { + parents: [__self__], + layer: layer, + listeners: [], + query_range_nm: 25, + query_type:'vor', + }; + return m; +}; +var del = func() { + #print("VOR.lcontroller.del()"); + foreach (var l; me.listeners) + removelistener(l); +}; + +var searchCmd = func { + #print("Running query: WPT"); + + var fp = flightplan(); + var fpSize = fp.getPlanSize(); + var result = []; + for (var i = 1; i 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: 'wpActiveId', + impl: { + init: func(nd,symbol), + predicate: func(nd) getprop("/autopilot/route-manager/wp/id") != nil and getprop("autopilot/route-manager/active"), + is_true: func(nd) { + nd.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id")); + nd.symbols.wpActiveId.show(); + }, + is_false: func(nd) nd.symbols.wpActiveId.hide(), + }, # of wpActiveId.impl + }, # of wpActiveId + { + id: 'wpActiveDist', + impl: { + init: func(nd,symbol), + predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active"), + is_true: func(nd) { + nd.symbols.wpActiveDist.setText(sprintf("%3.01fNM",getprop("/autopilot/route-manager/wp/dist"))); + nd.symbols.wpActiveDist.show(); + }, + is_false: func(nd) nd.symbols.wpActiveDist.hide(), + }, # of wpActiveDist.impl + }, # of wpActiveDist + { + id: 'eta', + impl: { + init: func(nd,symbol), + predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil and getprop("autopilot/route-manager/active"), + is_true: func(nd) { + var etaSec = getprop("/sim/time/utc/day-seconds")+getprop("autopilot/route-manager/wp/eta-seconds"); + var h = math.floor(etaSec/3600); + if (h>24) h=h-24; + etaSec=etaSec-3600*h; + var m = math.floor(etaSec/60); + etaSec=etaSec-60*m; + var s = etaSec; + 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_mag() )), + 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:'rangeArcs', + impl: { + init: func(nd,symbol), + predicate: func(nd) (((nd.get_switch('toggle_display_mode') == "APP" or nd.get_switch('toggle_display_mode') == "VOR") and nd.get_switch('toggle_weather')) or nd.get_switch('toggle_display_mode') == "MAP"), + is_true: func(nd) nd.symbols.rangeArcs.show(), + is_false: func(nd) nd.symbols.rangeArcs.hide(), + }, # of rangeArcs.impl + }, # of rangeArcs + + + ], # end of vector with features + + + }, # end of Boeing style +##### +## +## add support for other aircraft/ND types and styles here (Airbus etc) ## -## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map) ## - layers: [ - { name:'fixes', update_on:['toggle_range','toggle_waypoints','toggle_display_mode'], predicate: func(nd, layer) { - var visible=nd.get_switch('toggle_waypoints') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); - if(visible) - trigger_update( layer ); - layer._view.setVisible(visible); - }, # end of layer update predicate - }, # end of fixes layer - - # Should redraw every 10 seconds - { name:'storms', update_on:['toggle_range','toggle_weather','toggle_display_mode'], predicate: func(nd, layer) { - var visible=nd.get_switch('toggle_weather') and nd.get_switch('toggle_display_mode') != "PLAN"; - if (visible) - trigger_update( layer ); - layer._view.setVisible(visible); - }, # end of layer update predicate - }, # end of storms layer - - { name:'airplaneSymbol', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { - var visible=nd.get_switch('toggle_display_mode') == "PLAN"; - if (visible) - trigger_update( layer ); - layer._view.setVisible(visible); - }, - }, - - { name:'airports-nd', update_on:['toggle_range','toggle_airports','toggle_display_mode'], predicate: func(nd, layer) { - var visible = nd.get_switch('toggle_airports') and nd.in_mode('toggle_display_mode', ['MAP']); - if (visible) - trigger_update( layer ); # clear & redraw - layer._view.setVisible( visible); - }, # end of layer update predicate - }, # end of airports layer - - # Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag. - { name:'vor', update_on:['toggle_range','toggle_stations','toggle_display_mode'], predicate: func(nd, layer) { - var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); - if(visible) - trigger_update( layer ); # clear & redraw - layer._view.setVisible( visible ); - }, # end of layer update predicate - }, # end of VOR layer - - # Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag. - { name:'dme', update_on:['toggle_range','toggle_stations','toggle_display_mode'], predicate: func(nd, layer) { - var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); - if(visible) - trigger_update( layer ); # clear & redraw - layer._view.setVisible( visible ); - }, # 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( 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") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']) ; - 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_display_mode',], predicate: func(nd, layer) { - var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN'])); - if (visible) - trigger_update( layer ); # clear & redraw - layer._view.setVisible( visible ); - }, # end of layer update predicate - }, # end of route 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: 'wpActiveId', - impl: { - init: func(nd,symbol), - predicate: func(nd) getprop("/autopilot/route-manager/wp/id") != nil and getprop("autopilot/route-manager/active"), - is_true: func(nd) { - nd.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id")); - nd.symbols.wpActiveId.show(); - }, - is_false: func(nd) nd.symbols.wpActiveId.hide(), - }, # of wpActiveId.impl - }, # of wpActiveId - { - id: 'wpActiveDist', - impl: { - init: func(nd,symbol), - predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active"), - is_true: func(nd) { - nd.symbols.wpActiveDist.setText(sprintf("%3.01fNM",getprop("/autopilot/route-manager/wp/dist"))); - nd.symbols.wpActiveDist.show(); - }, - is_false: func(nd) nd.symbols.wpActiveDist.hide(), - }, # of wpActiveDist.impl - }, # of wpActiveDist - { - id: 'eta', - impl: { - init: func(nd,symbol), - predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil and getprop("autopilot/route-manager/active"), - is_true: func(nd) { - var etaSec = getprop("/sim/time/utc/day-seconds")+getprop("autopilot/route-manager/wp/eta-seconds"); - var h = math.floor(etaSec/3600); - if (h>24) h=h-24; - etaSec=etaSec-3600*h; - var m = math.floor(etaSec/60); - etaSec=etaSec-60*m; - var s = etaSec; - 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_mag() )), - 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:'rangeArcs', - impl: { - init: func(nd,symbol), - predicate: func(nd) ((nd.in_mode('toggle_display_mode', ['APP','VOR']) and nd.get_switch('toggle_weather')) or (nd.get_switch('toggle_display_mode') == "MAP" and !nd.get_switch('toggle_centered'))), - is_true: func(nd) nd.symbols.rangeArcs.show(), - is_false: func(nd) nd.symbols.rangeArcs.hide(), - }, # of rangeArcs.impl - }, # of rangeArcs - - ], # end of vector with features - - - }, # end of Boeing ND style -}; +}; # 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 -{ +NDSourceDriver.new = func { var m = {parents:[NDSourceDriver]}; m.get_hdg_mag= func getprop("/orientation/heading-magnetic-deg"); m.get_hdg_tru= func getprop("/orientation/heading-deg"); @@ -246,7 +304,7 @@ NDSourceDriver.new = func { if(getprop("/velocities/groundspeed-kt") > 80) { - getprop("/orientation/track-magnetic-deg"); + getprop("/orientation/track-magnetic-deg"); } else { @@ -273,8 +331,8 @@ NDSourceDriver.new = func ## # 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 +# 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. @@ -282,21 +340,21 @@ NDSourceDriver.new = func # TODO: switches are ND specific, so move to the NDStyle hash! var default_switches = { - 'toggle_range': {path: '/inputs/range-nm', value:10, 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/tfc',value:0, type:'BOOL'}, - 'toggle_centered': {path: '/inputs/nd-centered',value:0, type:'BOOL'}, - 'toggle_lh_vor_adf': {path: '/inputs/lh-vor-adf',value:0, type:'INT'}, - 'toggle_rh_vor_adf': {path: '/inputs/rh-vor-adf',value:0, type:'INT'}, - 'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'}, # valid values are: APP, MAP, PLAN or VOR - 'toggle_display_type': {path: '/mfd/display-type', value:'CRT', type:'STRING'}, # valid values are: CRT or LCD - 'toggle_true_north': {path: '/mfd/true-north', value:0, type:'BOOL'}, + '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_centered': {path: '/inputs/nd-centered',value:0, type:'BOOL'}, + 'toggle_lh_vor_adf': {path: '/inputs/lh-vor-adf',value:0, type:'INT'}, + 'toggle_rh_vor_adf': {path: '/inputs/rh-vor-adf',value:0, type:'INT'}, + 'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'}, # valid values are: APP, MAP, PLAN or VOR + 'toggle_display_type': {path: '/mfd/display-type', value:'CRT', type:'STRING'}, # valid values are: CRT or LCD + 'toggle_true_north': {path: '/mfd/true-north', value:0, type:'BOOL'}, }; # Hack to update weather radar once every 10 seconds @@ -316,239 +374,268 @@ var update_apl_sym = func { update_apl_sym(); ## -# TODO: +# 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 - { +var NavDisplay = { + + # reset handler + handle_reinit: func { + print("Cleaning up NavDisplay listeners"); # shut down all timers and other loops here - me.update_timer.stop(); + 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) - { - me.listen( me.get_full_switch_path(s), func - { - 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 ~ - }, + listen: func(p,c) { + append(me.listeners, setlistener(p,c)); + }, - # helper method for getting configurable cockpit switches (which are usually different in each aircraft) - get_switch: func(s) - { + # 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 = - { + # 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_mag: func source.getNode('orientation/heading-magnetic-deg').getValue(), - get_hdg_tru: func source.getNode('orientation/heading-deg').getValue(), get_trk_mag: func source.getNode('orientation/track-magnetic-deg').getValue(), - get_trk_tru: func source.getNode('orientation/track-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(), - get_vspd: func source.getNode('velocities/vertical-speed-fps').getValue(), }; - }, # of connectAI + }, # 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='Boeing') - { + # especially properties and SVG files/handles (747, 757, 777 etc) + new : func(prop1, switches=default_switches, style='Boeing') { 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.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.std_mode = m.efis.initNode("inputs/setting-std",0,"BOOL"); - m.previous_set = m.efis.initNode("inhg-previous",29.92); - 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.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.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # ditto - return m; + + 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.std_mode = m.efis.initNode("inputs/setting-std",0,"BOOL"); + m.previous_set = m.efis.initNode("inhg-previous",29.92); + 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.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.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # not yet in switches hash + + ### + # 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 ) { - 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) + # 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; ["wind","dmeLDist","dmeRDist","dmeL","dmeR","vorL","vorR","vorLId","vorRId", - "range","status.wxr","status.wpt","hdgGroup","status.sta","status.arpt"]) - me.symbols[element] = me.nd.getElementById(element); + "range","status.wxr","status.wpt","hdgGroup","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; ["windArrow","compassApp","northUp","aplSymMap","aplSymMapCtr","aplSymVor", - "staFromL2","staToL2","staFromR2","staToR2", - "locPtr","hdgTrk","truMag","altArc","planArcs", - "trkInd","compass","HdgBugCRT","TrkBugLCD","HdgBugLCD","selHdgLine","curHdgPtr", - "staFromL","staToL","staFromR","staToR"] ) - me.symbols[element] = me.nd.getElementById(element).updateCenter(); + "staFromL2","staToL2","staFromR2","staToR2", + "locPtr","hdgTrk","truMag","altArc","planArcs", + "trkInd","compass","HdgBugCRT","TrkBugLCD","HdgBugLCD","selHdgLine","curHdgPtr", + "staFromL","staToL","staFromR","staToR"] ) + me.symbols[element] = me.nd.getElementById(element).updateCenter(); foreach(var element; ["HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","selHdgLine2","curHdgPtr2","vorCrsPtr2"] ) - me.symbols[element] = me.nd.getElementById(element).setCenter(512,565); + me.symbols[element] = me.nd.getElementById(element).setCenter(512,565); # this should probably be using Philosopher's new SymbolLayer ? me.map = me.nd.createChild("map","map") .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 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; + 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")) + 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 + 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()]; + 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, + # + var controller = { + query_range: func get_range(), + is_tuned:is_tuned, get_tuned_course:get_course_by_freq, get_position: get_current_position, - }; - + }; + + # FIXME: MapStructure: big hack + 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; + ### # 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) - { + foreach(var layer; me.nd_style.layers) { + if(layer['disabled']) continue; # skip this layer + #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 ); + + var the_layer = nil; + if(!layer['isMapStructure']) + the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller ); + else { + #print("Setting up MapStructure-based layer for ND, name:", layer.name); + render_target.addLayer(factory: canvas.SymbolLayer, type_arg: layer.name); + the_layer = me.layers[layer.name] = render_target.getLayer(layer.name); + } # 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) - { - me.listen_switch(event, event_handler ) ; + 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 - # start the update timer, which makes sure that the update() will be called + #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(); - }); + 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 # Hack to draw the route on rm activation - me.listen("/autopilot/route-manager/active", func(active) - { - if(active.getValue()) - { - setprop(me.get_full_switch_path('toggle_display_mode'),getprop(me.get_full_switch_path('toggle_display_mode'))); - } - else - { + 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) - { + 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; @@ -563,21 +650,20 @@ var NavDisplay = # important constants var m1 = 111132.92; var m2 = -559.82; - var m3 = 1.175; + var m3 = 1.175; var m4 = -0.0023; - var p1 = 111412.84; - var p2 = -93.5; - var p3 = 0.118; + var p1 = 111412.84; + var p2 = -93.5; + var p3 = 0.118; var latNm = 60; var lonNm = 60; # fgcommand('profiler-start'); - # Heading update - var userHdgMag = me.aircraft_source.get_hdg_mag(); - var userHdgTru = me.aircraft_source.get_hdg_tru(); - var userTrkMag = me.aircraft_source.get_trk_mag(); - var userTrkTru = me.aircraft_source.get_trk_tru(); + var userHdgMag = me.aircraft_source.get_hdg_mag(); + var userHdgTru = me.aircraft_source.get_hdg_tru(); + var userTrkMag = me.aircraft_source.get_trk_mag(); + var userTrkTru = me.aircraft_source.get_trk_tru(); if(me.get_switch('toggle_true_north')) { me.symbols.truMag.setText("TRU"); var userHdg=userHdgTru; @@ -588,7 +674,7 @@ var NavDisplay = var userTrk=userTrkMag; } if (me.aircraft_source.get_spd() < 80) - userTrk = userHdg; + userTrk = userHdg; var userLat = me.aircraft_source.get_lat(); var userLon = me.aircraft_source.get_lon(); var userSpd = me.aircraft_source.get_spd(); @@ -600,24 +686,24 @@ var NavDisplay = print("aircraft source invalid, returning !"); return; } - + if(me.in_mode('toggle_display_mode', ['PLAN'])) me.map.setTranslation(512,512); elsif(me.get_switch('toggle_centered')) me.map.setTranslation(512,565); else me.map.setTranslation(512,824); - # Calculate length in NM of one degree at current location TODO: expose as methods, for external callbacks + # 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(me.get_switch('toggle_lh_vor_adf') == 1) { me.symbols.vorL.setText("VOR L"); @@ -681,10 +767,10 @@ var NavDisplay = me.symbols.vorRId.setText(""); me.symbols.dmeRDist.setText(""); } - + me.symbols.range.setText(sprintf("%3.0f",me.rangeNm()/2)); - # reposition the map, change heading & range: + # reposition the map, change heading & range: if(me.in_mode('toggle_display_mode', ['PLAN'])) { me.symbols.windArrow.setVisible(!dispLCD); me.map._node.getNode("hdg",1).setDoubleValue(0); @@ -738,7 +824,7 @@ var NavDisplay = me.symbols.compassApp.show(); if(getprop("instrumentation/nav/in-range")) { var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm"); - me.symbols.locPtr.show(); + me.symbols.locPtr.show(); me.symbols.locPtr.setTranslation(deflection*150,0); if(abs(deflection < 0.99)) me.symbols.locPtr.setColorFill(1,0,1,1); @@ -759,13 +845,13 @@ var NavDisplay = me.symbols.hdgGroup.setTranslation(0,0); me.symbols.compassApp.hide(); } - + if ((me.get_switch('toggle_centered') and !me.in_mode('toggle_display_mode', ['PLAN'])) or me.in_mode('toggle_display_mode', ['PLAN'])) { me.symbols.compass.hide(); } else { me.symbols.compass.show(); } - + var staPtrVis = !me.in_mode('toggle_display_mode', ['APP','PLAN']); var magVar = getprop("environment/magnetic-variation-deg"); if(me.in_mode('toggle_display_mode', ['APP','MAP','VOR','PLAN'])) @@ -887,14 +973,14 @@ var NavDisplay = me.symbols.selHdgLine2.setVisible(staPtrVis); } } - + me.symbols.hdgGroup.setVisible(!me.in_mode('toggle_display_mode', ['PLAN'])); me.symbols.northUp.setVisible(me.in_mode('toggle_display_mode', ['PLAN'])); me.symbols.aplSymMap.setVisible(me.in_mode('toggle_display_mode', ['APP','MAP','VOR']) and !me.get_switch('toggle_centered')); me.symbols.aplSymMapCtr.setVisible(me.in_mode('toggle_display_mode', ['MAP']) and me.get_switch('toggle_centered')); me.symbols.aplSymVor.setVisible(me.in_mode('toggle_display_mode', ['APP','VOR']) and me.get_switch('toggle_centered')); me.symbols.planArcs.setVisible(me.in_mode('toggle_display_mode', ['PLAN'])); - + if (abs(userVSpd) > 5) { var altDiff = getprop("autopilot/settings/target-altitude-ft")-getprop("instrumentation/altimeter/indicated-altitude-ft"); if (abs(altDiff) > 50 and altDiff/userVSpd > 0) { @@ -911,7 +997,7 @@ var NavDisplay = } else { me.symbols.altArc.hide(); } - + ## 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 @@ -931,10 +1017,9 @@ var NavDisplay = ## 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') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') and me.in_mode('toggle_display_mode', ['MAP'])); } -}; \ No newline at end of file +}; diff --git a/Nasal/canvas/map/route.model b/Nasal/canvas/map/route.model index 80df9e496..27dfc3487 100644 --- a/Nasal/canvas/map/route.model +++ b/Nasal/canvas/map/route.model @@ -4,8 +4,8 @@ RouteModel.new = func make(LayerModel, RouteModel); RouteModel.init = func { me._view.reset(); - if (!getprop("/autopilot/route-manager/active")) - return; + if (!getprop("/autopilot/route-manager/active")) + return; ## 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] )