diff --git a/Nasal/canvas/MapStructure.nas b/Nasal/canvas/MapStructure.nas new file mode 100644 index 000000000..39453056a --- /dev/null +++ b/Nasal/canvas/MapStructure.nas @@ -0,0 +1,374 @@ +var Symbol = { +# Static/singleton: + registry: {}, + add: func(type, class) + me.registry[type] = class, + get: func(type) + if ((var class = me.registry[type]) == nil) + die("unknown type '"~type~"'"); + else return class, + # Calls corresonding symbol constructor + # @param group #Canvas.Group to place this on. + new: func(type, group, arg...) { + var ret = call((var class = me.get(type)).new, [group]~arg, class); + ret.element.set("symbol-type", type); + return ret; + }, +# Non-static: + df_controller: nil, # default controller + # Update the drawing of this object (position and others). + update: func() + die("update() not implemented for this symbol type!"), + draw: func(group, model, lod) + die("draw() not implemented for this symbol type!"), + del: func() + die("del() not implemented for this symbol type!"), +}; # of Symbol + +Symbol.Controller = { +# Static/singleton: + registry: {}, + add: func(type, class) + registry[type] = class, + get: func(type) + if ((var class = me.registry[type]) == nil) + die("unknown type '"~type~"'"); + else return class, + # Calls corresonding symbol constructor + # @param model Model to place this on. + new: func(type, model, arg...) + return call((var class = me.get(type)).new, [model]~arg, class), +# Non-static: + # Update anything related to a particular model. Returns whether the object needs updating: + update: func(model) return 1, + # Initialize a controller to an object (or initialize the controller itself): + init: func(model) , + # Delete an object from this controller (or delete the controller itself): + del: func(model) , + # Return whether this symbol/object is visible: + isVisible: func(model) return 1, + # Get the position of this symbol/object: + getpos: func(model), # default provided below +}; # of Symbol.Controller + +var getpos_fromghost = func(positioned_g) + return [positioned_g.lat, positioned_g.lon]; + +# Generic getpos: get lat/lon from any object: +# (geo.Coord and positioned ghost currently) +Symbol.Controller.getpos = func(obj) { + if (typeof(obj) == 'ghost') + if (ghosttype(obj) == 'positioned' or ghosttype(obj) == 'Navaid') + return getpos_fromghost(obj); + else + die("bad ghost of type '"~ghosttype(obj)~"'"); + if (typeof(obj) == 'hash') + if (isa(obj, geo.Coord)) + return obj.latlon(); + die("no suitable getpos() found! Of type: "~typeof(obj)); +}; + + +var assert_m = func(hash, member) + if (!contains(hash, member)) + die("required field not found: '"~member~"'"); +var assert_ms = func(hash, members...) + foreach (var m; members) + if (m != nil) assert_m(hash, m); + + +var DotSym = { + parents: [Symbol], + element_id: nil, +# Static/singleton: + makeinstance: func(name, hash) { + assert_ms(hash, + "element_type", # type of Canvas element + #"element_id", # optional Canvas id + #"init", # initialize routine + "draw", # init/update routine + #getpos", # get position from model in [x_units,y_units] (optional) + ); + hash.parents = [DotSym]; + return Symbol.add(name, hash); + }, + readinstance: func(file, name=nil) { + #print(file); + if (name == nil) + var name = split("/", file)[-1]; + if (substr(name, size(name)-4) == ".draw") + name = substr(name, 0, size(name)-5); + var code = io.readfile(file); + var code = call(compile, [code], var err=[]); + if (size(err)) { + if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature + var e = split(" at line ", err[0]); + if (size(e) == 2) + err[0] = string.join("", [e[0], "\n at ", file, ", line ", e[1], "\n "]); + } + for (var i = 1; (var c = caller(i)) != nil; i += 1) + err ~= subvec(c, 2, 2); + debug.printerror(err); + return; + } + call(code, nil, nil, var hash = { parents:[DotSym] }); + me.makeinstance(name, hash); + }, +# For the instances returned from makeinstance: + # @param group The Canvas group to add this to. + # @param model A correct object (e.g. positioned ghost) as + # expected by the .draw file that represents + # metadata like position, speed, etc. + # @param controller Optional controller "glue". Each method + # is called with the model as the only argument. + new: func(group, model, controller=nil) { + var m = { + parents: [me], + group: group, + model: model, + controller: controller == nil ? me.df_controller : controller, + element: group.createChild( + me.element_type, me.element_id + ), + }; + if (m.controller != nil) { + #print("Initializing controller"); + m.controller.init(model); + } + else die("default controller not found"); + + m.init(); + return m; + }, + del: func() { + #print("DotSym.del()"); + me.deinit(); + if (me.controller != nil) + me.controller.del(me.model); + call(func me.model.del(), nil, var err=[]); # try... + if (err[0] != "No such member: del") # ... and either catch or rethrow + die(err[0]); + me.element.del(); + }, +# Default wrappers: + init: func() me.draw(), + deinit: func(), + update: func() { + if (me.controller != nil) { + if (!me.controller.update(me.model)) return; + elsif (!me.controller.isVisible(me.model)) { + me.element.hide(); + return; + } + } else + me.element.show(); + me.draw(); + var pos = me.controller.getpos(me.model); + if (size(pos) == 2) + pos~=[nil]; # fall through + if (size(pos) == 3) + var (lat,lon,rotation) = pos; + else die("bad position: "~debug.dump(pos)); + me.element.setGeoPosition(lat,lon); + if (rotation != nil) + me.element.setRotation(rotation); + }, +}; # of DotSym + +# A layer that manages a list of symbols (using delta positioned handling). +var SymbolLayer = { +# Static/singleton: + registry: {}, + add: func(type, class) + me.registry[type] = class, + get: func(type) + if ((var class = me.registry[type]) == nil) + die("unknown type '"~type~"'"); + else return class, +# Non-static: + df_controller: nil, # default controller + df_priority: nil, # default priority for display sorting + type: nil, # type of #Symbol to add (MANDATORY) + id: nil, # id of the group #canvas.Element (OPTIONAL) + # @param group A group to place this on. + # @param controller A controller object (parents=[SymbolLayer.Controller]) + # or implementation (parents[0].parents=[SymbolLayer.Controller]). + new: func(group, controller=nil) { + var m = { + parents: [me], + group: group.createChild("group", me.id), # TODO: the id is not properly set, but would be useful for debugging purposes (VOR, FIXES, NDB etc) + list: [], + }; + # FIXME: hack to expose type of layer: + if (caller(1)[1] == Map.addLayer) { + var this_type = caller(1)[0].type_arg; + if (this_type != nil) + m.group.set("symbol-layer-type", this_type); + } + if (controller == nil) + #controller = SymbolLayer.Controller.new(me.type, m); + controller = me.df_controller; + assert_m(controller, "parents"); + if (controller.parents[0] == SymbolLayer.Controller) + controller = controller.new(m); + assert_m(controller, "parents"); + assert_m(controller.parents[0], "parents"); + if (controller.parents[0].parents[0] != SymbolLayer.Controller) + die("OOP error"); + m.controller = controller; + m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m); + m.update(); + return m; + }, + update: func() { + me.searcher.update(); + foreach (var e; me.list) + e.update(); + }, + del: func() { + #print("SymbolLayer.del()"); + me.controller.del(); + foreach (var e; me.list) + e.del(); + }, + findsym: func(positioned_g, del=0) { + forindex (var i; me.list) { + var e = me.list[i]; + if (geo.PositionedSearch._equals(e.model, positioned_g)) { + if (del) { + # Remove this element from the list + var prev = subvec(me.list, 0, i); + var next = subvec(me.list, i+1); + me.list = prev~next; + } + return e; + } + } + return nil; + }, + searchCmd: func() me.controller.searchCmd(), + # Adds a symbol. + onAdded: func(positioned_g) + append(me.list, Symbol.new(me.type, me.group, positioned_g)), + # Removes a symbol + onRemoved: func(positioned_g) + me.findsym(positioned_g, 1).del(), +}; # of SymbolLayer + +# Class to manage controlling a #SymbolLayer. +# Currently handles: +# * Searching for new symbols (positioned ghosts or other objects with unique id's). +# * Updating the layer (e.g. on an update loop or on a property change). +SymbolLayer.Controller = { +# Static/singleton: + registry: {}, + add: func(type, class) + me.registry[type] = class, + get: func(type) + if ((var class = me.registry[type]) == nil) + die("unknown type '"~type~"'"); + else return class, + # Calls corresonding controller constructor + # @param layer The #SymbolLayer this controller is responsible for. + new: func(type, layer, arg...) + return call((var class = me.get(type)).new, [layer]~arg, class), +# Non-static: + run_update: func() { + me.layer.update(); + }, + # @return List of positioned objects. + searchCmd: func() + die("searchCmd() not implemented for this SymbolLayer.Controller type!"), +}; # of SymbolLayer.Controller + +settimer(func { +Map.Controller = { +# Static/singleton: + registry: {}, + add: func(type, class) + me.registry[type] = class, + get: func(type) + if ((var class = me.registry[type]) == nil) + die("unknown type '"~type~"'"); + else return class, + # Calls corresonding controller constructor + # @param map The #SymbolMap this controller is responsible for. + new: func(type, layer, arg...) + return call((var class = me.get(type)).new, [map]~arg, class), +}; + +####### LOAD FILES ####### +#print("loading files"); +(func { + var FG_ROOT = getprop("/sim/fg-root"); + var load = func(file, name) { + #print(file); + if (name == nil) + var name = split("/", file)[-1]; + if (substr(name, size(name)-4) == ".draw") + name = substr(name, 0, size(name)-5); + #print("reading file"); + var code = io.readfile(file); + #print("compiling file"); + # This segfaults for some reason: + #var code = call(compile, [code], var err=[]); + var code = call(func compile(code, file), [code], var err=[]); + if (size(err)) { + #print("handling error"); + if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature + var e = split(" at line ", err[0]); + if (size(e) == 2) + err[0] = string.join("", [e[0], "\n at ", file, ", line ", e[1], "\n "]); + } + for (var i = 1; (var c = caller(i)) != nil; i += 1) + err ~= subvec(c, 2, 2); + debug.printerror(err); + return; + } + #print("calling code"); + call(code, nil, nil, var hash = {}); + #debug.dump(keys(hash)); + return hash; + }; + load(FG_ROOT~"/Nasal/canvas/map/VOR.lcontroller", "VOR"); + DotSym.readinstance(FG_ROOT~"/Nasal/canvas/map/VOR.symbol", "VOR"); + load(FG_ROOT~"/Nasal/canvas/map/VOR.scontroller", "VOR"); + load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", "VOR"); +})(); +#print("finished loading files"); +####### TEST SYMBOL ####### + +if (0) +settimer(func { + if (caller(0)[0] != globals.canvas) + return call(caller(0)[1], arg, nil, globals.canvas); + + print("Running MapStructure test code"); + var TestCanvas = canvas.new({ + "name": "Map Test", + "size": [1024, 1024], + "view": [1024, 1024], + "mipmapping": 1 + }); + var dlg = canvas.Window.new([400, 400], "dialog"); + dlg.setCanvas(TestCanvas); + var TestMap = TestCanvas.createGroup().createChild("map"); # we should not directly use a canvas here, but instead a LayeredMap.new() + TestMap.addLayer(factory: SymbolLayer, type_arg: "VOR"); # the ID should be also exposed in the property tree for each group (layer), i.e. better debugging + # Center the map's origin: + TestMap.setTranslation(512,512); # FIXME: don't hardcode these values, but read in canvas texture dimensions, otherwise it will break once someone uses non 1024x1024 textures ... + # Initialize a range (TODO: LayeredMap.Controller): + TestMap.set("range", 100); + # Little cursor of current position: + TestMap.createChild("path").rect(-5,-5,10,10).setColorFill(1,1,1).setColor(0,1,0); + # And make it move with our aircraft: + TestMap.setController("Aircraft position"); # from aircraftpos.controller + dlg.del = func() { + TestMap.del(); + # call inherited 'del' + delete(me, "del"); + me.del(); + }; +}, 1); +else print("MapStructure.nas: Testing code disabled, see $FG_ROOT/gui/dialogs/map-canvas.xml instead"); +}, 0); # end ugly module init timer hack + diff --git a/Nasal/canvas/api.nas b/Nasal/canvas/api.nas index d2f056f1f..8cccc71f8 100644 --- a/Nasal/canvas/api.nas +++ b/Nasal/canvas/api.nas @@ -419,11 +419,90 @@ var Group = { # which automatically get projected according to the specified projection. # var Map = { + df_controller: nil, new: func(ghost) { return { parents: [Map, Group.new(ghost)] }; - } - # TODO + }, + del: func() + { + #print("canvas.Map.del()"); + call(func { + me.controller.del(me); + }, var err=[]); + if (size(err)) { + debug.printerror(err); + setsize(err, 0); + } + call(func { + foreach (var l; me.layers) + call(l[0].del, nil, l[0]); + setsize(me.layers, 0); + }, err); + if (size(err)) { + debug.printerror(err); + setsize(err, 0); + } + # call inherited 'del' + me.parents = subvec(me.parents,1); + me.del(); + }, + setController: func(controller=nil) + { + if (controller == nil) + controller = Map.df_controller; + elsif (typeof(controller) != 'hash') + controller = Map.Controller.get(controller); + if (controller.parents[0] != Map.Controller) + die("OOP error"); + me.controller = controller.new(me); + + return me; + }, + addLayer: func(factory, type_arg=nil, priority=nil) + { + if (!contains(me, "layers")) + me.layers = []; + + # Argument handling + if (type_arg != nil) + var type = factory.get(type_arg); + else var type = factory; + + if (priority == nil) + priority = type.df_priority; + append(me.layers, [type.new(me), priority]); + if (priority != nil) + me._sort_priority(); + return me; + }, + setPos: func(lat,lon,hdg=nil) + { + me.set("ref-lat", lat); + me.set("ref-lon", lon); + if (hdg != nil) + me.set("hdg", hdg); + + # me.map.set("range", 100); + }, + # Update each layer on this Map. Called by + # me.controller. + update: func + { + foreach (var l; me.layers) + call(l[0].update, arg, l[0]); + return me; + }, +# private: + _sort_priority: func() + { + me.layers = sort(me.layers, me._sort_cmp); + forindex (var i; me.layers) + me.layers[i].set("z-index", i); + }, + _sort_cmp: func(a,b) { + a[1] != b[1] and a[1] != nil and b[1] != nil and (a[1] < b[1] ? -1 : 1) + }, }; # Text @@ -442,7 +521,7 @@ var Text = { }, # Set alignment # - # @param algin String, one of: + # @param align String, one of: # left-top # left-center # left-bottom @@ -653,8 +732,8 @@ var Path = { cubicTo: func me.addSegment(me.VG_CUBIC_TO_ABS, arg), cubic: func me.addSegment(me.VG_CUBIC_TO_REL, arg), # Add a smooth quadratic Bézier curve - quadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg), - quad: func me.addSegment(me.VG_SQUAD_TO_REL, arg), + squadTo: func me.addSegment(me.VG_SQUAD_TO_ABS, arg), + squad: func me.addSegment(me.VG_SQUAD_TO_REL, arg), # Add a smooth cubic Bézier curve scubicTo: func me.addSegment(me.VG_SCUBIC_TO_ABS, arg), scubic: func me.addSegment(me.VG_SCUBIC_TO_REL, arg), diff --git a/Nasal/canvas/map.nas b/Nasal/canvas/map.nas index 751541090..1b91fe76b 100644 --- a/Nasal/canvas/map.nas +++ b/Nasal/canvas/map.nas @@ -1,7 +1,7 @@ ### # map.nas - provide a high level method to create typical maps in FlightGear (airports, navaids, fixes and waypoints) for both, the GUI and instruments # implements the notion of a "layer" by using canvas groups and adding geo-referenced elements to a layer -# layered maps are linked to boolean properties so that visibility can be easily toggled (GUI checkboxes or cockpit hotspots) +# layered maps are linked to boolean properties so that visibility can be easily toggled (via GUI checkboxes or cockpit hotspots) # without having to redraw other layers # # GOALS: have a single Nasal/Canvas wrapper for all sort of maps in FlightGear, that can be easily shared and reused for different purposes @@ -16,15 +16,17 @@ # ISSUES: just look for the FIXME and TODO strings - currently, the priority is to create an OOP/MVC design with less specialized code in XML files # # +# REGRESSIONS: 744 ND: toggle layer on/off, support different dialogs +# # ROADMAP: Generalize this further, so that: # # - it can be easily reused -# - use a MVC approach, where layer-specific data is provided by a Model object +# - it uses a MVC approach, where layer-specific data is provided by a Model object # - other dialogs can use this without tons of custom code (airports.xml, route-manager.xml, map-canvas.xml) -# - generalize this further so that it can be used by instruments +# - generalize this further so that it can be used by MFDs/instruments # - implement additional layers (tcas, wxradar, agradar) - especially expose the required data to Nasal # - implement better GUI support (events) so that zooming/panning via mouse can be supported -# - make the whole thing styleable +# - make the whole thing styleable # # - keep track of things getting added here and decide if they should better move to the core canvas module or the C++ code # @@ -33,7 +35,7 @@ # - overload findNavaidsWithinRange() to support an optional position argument, so that arbitrary navaids can be looked up # - add Nasal extension function to get scenery vector data (landclass) # - -# - +# - # @@ -47,28 +49,25 @@ var run_callbacks = func foreach(var c; callbacks) c(); var DEBUG=0; if (DEBUG) { var benchmark = debug.benchmark; - - } - -else { +} else { var benchmark = func(label, code) code(); # NOP - } +} var assert = func(label, expr) expr and die(label); -# Mapping from surface codes to #TODO: make this XML-configurable +# Mapping from surface codes to colors (shared by runways.draw and taxiways.draw) var SURFACECOLORS = { - 1 : { type: "asphalt", r:0.2, g:0.2, b:0.2 }, - 2 : { type: "concrete", r:0.3, g:0.3, b:0.3 }, - 3 : { type: "turf", r:0.2, g:0.5, b:0.2 }, - 4 : { type: "dirt", r:0.4, g:0.3, b:0.3 }, - 5 : { type: "gravel", r:0.35, g:0.3, b:0.3 }, -# Helipads - 6 : { type: "asphalt", r:0.2, g:0.2, b:0.2 }, - 7 : { type: "concrete", r:0.3, g:0.3, b:0.3 }, - 8 : { type: "turf", r:0.2, g:0.5, b:0.2 }, - 9 : { type: "dirt", r:0.4, g:0.3, b:0.3 }, - 0 : { type: "gravel", r:0.35, g:0.3, b:0.3 }, + 1 : { type: "asphalt", r:0.2, g:0.2, b:0.2 }, + 2 : { type: "concrete", r:0.3, g:0.3, b:0.3 }, + 3 : { type: "turf", r:0.2, g:0.5, b:0.2 }, + 4 : { type: "dirt", r:0.4, g:0.3, b:0.3 }, + 5 : { type: "gravel", r:0.35, g:0.3, b:0.3 }, +# Helipads + 6 : { type: "asphalt", r:0.2, g:0.2, b:0.2 }, + 7 : { type: "concrete", r:0.3, g:0.3, b:0.3 }, + 8 : { type: "turf", r:0.2, g:0.5, b:0.2 }, + 9 : { type: "dirt", r:0.4, g:0.3, b:0.3 }, + 0 : { type: "gravel", r:0.35, g:0.3, b:0.3 }, }; @@ -77,331 +76,197 @@ var SURFACECOLORS = { var draw_layer = func(layer, callback, lod) { var name= layer._view.get("id"); # print("Canvas:Draw op triggered"); # just to make sure that we are not adding unnecessary data when checking/unchecking a checkbox - if (DEBUG and name=="taxiways") fgcommand("profiler-start"); #without my patch, this is a no op, so no need to disable + #if (DEBUG and name=="taxiways") fgcommand("profiler-start"); #without my patch, this is a no op, so no need to disable #print("Work items:", size(layer._model._elements)); - benchmark("Drawing Layer:"~layer._view.get("id"), func + benchmark("Drawing Layer:"~layer._view.get("id"), func foreach(var element; layer._model._elements) { #print(typeof(layer._view)); #debug.dump(layer._view); - callback(layer._view, element, lod); # ISSUE here + callback(layer._view, element, layer._controller, lod); # ISSUE here }); if (! layer._model.hasData() ) print("Layer was EMPTY:", name); - if (DEBUG and name=="taxiways") fgcommand("profiler-stop"); - layer._drawn=1; #TODO: this should be encapsulated + #if (DEBUG and name=="taxiways") fgcommand("profiler-stop"); + layer._drawn=1; #TODO: this should be encapsulated } # Runway # var Runway = { - # Create Runway from hash - # - # @param rwy Hash containing runway data as returned from - # airportinfo().runways[ ] - new: func(rwy) - { - return { - parents: [Runway], - rwy: rwy - }; - }, - # Get a point on the runway with the given offset - # - # @param pos Position along the center line - # @param off Offset perpendicular to the center line - pointOffCenterline: func(pos, off = 0) - { - var coord = geo.Coord.new(); - coord.set_latlon(me.rwy.lat, me.rwy.lon); - coord.apply_course_distance(me.rwy.heading, pos); + # Create Runway from hash + # + # @param rwy Hash containing runway data as returned from + # airportinfo().runways[ ] + new: func(rwy) { + return { + parents: [Runway], + rwy: rwy + }; + }, + # Get a point on the runway with the given offset + # + # @param pos Position along the center line + # @param off Offset perpendicular to the center line + pointOffCenterline: func(pos, off = 0) { + var coord = geo.Coord.new(); + coord.set_latlon(me.rwy.lat, me.rwy.lon); + coord.apply_course_distance(me.rwy.heading, pos); - if( off ) - coord.apply_course_distance(me.rwy.heading + 90, off); + if(off) + coord.apply_course_distance(me.rwy.heading + 90, off); - return ["N" ~ coord.lat(), "E" ~ coord.lon()]; - } + return ["N" ~ coord.lat(), "E" ~ coord.lon()]; + } }; var make = func return {parents:arg}; -## -# TODO: Create a cache to reuse layers and layer data (i.e. runways) - - -## -# Todo: wrap parsesvg and return a function that memoizes the created canvas group, so that svg files only need to be parsed once -# - -## -# TODO: Implement a real MVC design for "LayeredMaps" that have: -# - a "DataProvider" (i.e. Positioned objects) -# - a View (i.e. a Canvas) -# - a controller (i.e. input/output properties) -# -var MapModel = {}; # navaids, waypoints, fixes etc - MapModel.new = func make(MapModel); - -var MapView = {}; # the canvas view, including a layer for each feature - MapView.new = func make(MapView); - -var MapController = {}; # the property tree interface to manipulate the model/view via properties - MapController.new = func make(MapController); - -var LazyView = {}; # Gets drawables on demand from the model - via property toggle - -var DataProvider = {}; - DataProvider.new = func make(DataProvider); - -### -# for airports, navaids, fixes, waypoints etc -var PositionedProvider = {}; - PositionedProvider.new = func make(DataProvider, PositionedProvider); - -## -# Drawable -# - -## LayerElement (UNUSED ATM): -# for runways, navaids, fixes, waypoints etc -# TODO: we should differentiate between "fairly static" vs. "dynamic" layers - i.e. navaids vs. traffic -var LayerElement = {_drawable:nil}; - LayerElement.new = func(drawable) { - var temp = make(LayerElement); - temp._drawable=drawable; - return temp; -} - # a drawable is either a Nasal callback or a scalar, i.e. a path to an SVG file - LayerElement.draw = func(group) { - (typeof(me._drawable)=='func') and drawable(group) or canvas.parsesvg(group,_drawable); - } - -# For static targets like Navaids, Fixes - i.e. geographic position doesn't change -var StaticLayerElement = {}; - -# For moving targets such as aircraft, multiplayer, ai traffic etc -var DynamicLayerElement = {}; - -var AnimatedLayerElement = {}; - -# for elements whose appearance may change depending on selected range (i.e. LOD) -var RangeAwareLayerElement = {}; - - ## # A layer model is just a wrapper for a vector with elements -# either updated via a timer or via a listener +# either updated via a timer or via a listener (or both) -var LayerModel = {_elements:[], _view:, _controller: }; - LayerModel.new = func make(LayerModel); - LayerModel.clear = func me._elements = []; - LayerModel.push = func (e) append(me._elements, e); - LayerModel.get = func me._elements; - LayerModel.update = func; - LayerModel.hasData = func size(me. _elements); - LayerModel.setView = func(v) me._view=v; - LayerModel.setController = func(c) me._controller=c; - - -var LayerController = {}; - LayerController.new = func make(LayerController); - -## -# use timers to update the model/view (canvas) -var TimeBasedLayerController = {}; - LayerController.new = func make(TimeBasedLayerController); - -## -# use listeners to update the model/view (canvas) -# -var ListenerBasedLayerController = {}; - ListenerBasedLayerController.new = func make(ListenerBasedLayerController); - - -## -# Uses, both, listeners and timers to update the model/view (canvas) -# - -var HybridLayerController = {}; - HybridLayerController.new = func make(HybridLayerController); - -var ModelEvents = {INIT:, RESET:, UPDATE:}; -var ViewEvents = {INIT:, RESET:, UPDATE:}; -var ControllerEvents = {INIT:, RESET: , UPDATE:, ZOOM:, PAN:, }; +var LayerModel = {_elements:[], _view:, _controller:{query_range:func 100}, }; +LayerModel.new = func make(LayerModel); +LayerModel.clear = func me._elements = []; +LayerModel.push = func (e) append(me._elements, e); +LayerModel.get = func me._elements; +LayerModel.update = func; +LayerModel.hasData = func size(me. _elements); +LayerModel.setView = func(v) me._view=v; +LayerModel.setController = func(c) me._controller=c; ## # A layer is mapped to a canvas group # Layers are linked to a single boolean property to toggle them on/off -var Layer = { _model: , - _view: , - _controller: , - _drawn:0, - }; +## FIXME: this is GUI specific ATM +var Layer = { + _model: , + _view: , + _controller: , + _drawn:0, +}; - Layer.new = func(group, name, model) { +Layer.new = func(group, name, model, controller=nil) { #print("Setting up new Layer:", name); - var m = make(Layer); + var m = make(Layer); m._model = model.new(); + if (controller!=nil) { + m._controller = controller; + m._model._controller = controller; + } + else # use the default controller (query_range for positioned queries =100nm) + m._controller = m._model._controller; + #print("Model name is:", m._model.name); - m._view = group.createChild("group",name); + m._view = group.createChild("group",name); + m._model._view = m; m.name = name; #FIXME: not needed, there's already _view.get("id") return m; } - Layer.hide = func me._view.setVisible(0); - Layer.show = func me._view.setVisible(1); - #TODO: Unify toggle and update methods - and support lazy drawing (make it optional!) - Layer.toggle = func { +Layer.hide = func me._view.setVisible(0); +Layer.show = func me._view.setVisible(1); +#TODO: Unify toggle and update methods - and support lazy drawing (make it optional!) +Layer.toggle = func { # print("Toggling layer"); var checkbox = getprop(me.display_layer); - if(checkbox and !me._drawn) { + if(checkbox and !me._drawn) { # print("Lazy drawing"); me.draw(); } - + #var state= me._view.getBool("visible"); #print("Toggle layer visibility ",me.display_layer," checkbox is", checkbox); #print("Layer id is:", me._view.get("id")); #print("Drawn is:", me._drawn); checkbox?me._view.setVisible(1) : me._view.setVisible(0); - } - Layer.reset = func { - me._view.removeAllChildren(); # clear the "real" canvas drawables - me._model.clear(); # the vector is used for lazy rendering - assert("Model not emptied during layer reset!", me._model.hasData() ); - me._drawn = 0; - } - #TODO: Unify toggle and update - Layer.update = func { - # print("Layer update: Check if layer is visible, if so, draw"); - if (! getprop(me.display_layer)) return; # checkbox for layer not set - if (!me._model.hasData() ) return; # no data available - # print("Trying to draw"); - me.draw(); +} +Layer.reset = func { + me._view.removeAllChildren(); # clear the "real" canvas drawables + me._model.clear(); # the vector is used for lazy rendering + assert("Model not emptied during layer reset!", me._model.hasData() ); + me._drawn = 0; +} +#TODO: Unify toggle and update FIXME: GUI specific, not needed for 744 ND.nas +Layer.update = func { + # print("Layer update: Check if layer is visible, if so, draw"); + if (contains(me, "display_layer")) #UGLY HACK + if (! getprop(me.display_layer)) return; # checkbox for layer not set + + if (!me._model.hasData() ) return; # no data available + # print("Trying to draw"); + me.draw(); } -Layer.setDraw = func(callback) me.draw = callback; +Layer.setDraw = func(callback) me.draw = callback; Layer.setController = func(c) me._controller=c; # TODO: implement Layer.setModel = func(m) nil; # TODO: implement -##TODO: differentiate between layers with a single object (i.e. aircraft) and multiple objects (airports) - -## -# We may need to display some stuff that isn't strictly a geopgraphic feature, but just a chart feature -# -var CartographicLayer = {}; - -#TODO: -var InteractiveLayer = {}; - -### -# PositionedLayer -# -# layer of positioned objects (i.e. have lat,lon,alt) -# -var PositionedLayer = {}; - PositionedLayer.new = func() { - make( Layer.new() , PositionedLayer ); - } - - -### -# CachedLayer -# -# when re-centering on an airport already loaded, we don't want to reload it -# but change the reference point and load missing airports - -var CachedLayer = {}; - -## -# -var AirportProvider = {}; - AirportProvider.new = func make(AirportProvider); - AirportProvider.get = func { - return airportinfo("ksfo"); -} - -### Data Providers (preparation for MVC version): -# TODO: should use the LayerModel class -# - -## -# Manage a bunch of layers -# - -var LayerManager = {}; - -# WXR ? - -# TODO: Stub -var MapBehavior = {}; - MapBehavior.new = make(MapBehavior); - MapBehavior.zoom = func; - MapBehavior.center = func; ## # A layered map consists of several layers # TODO: Support nested LayeredMaps, where a LayeredMap may contain other LayeredMaps -# TODO: use MapBehavior here and move the zoom/refpos methods there, so that map behavior can be easily customized -var LayeredMap = { ranges:[], - zoom_property:nil, listeners:[], - update_property:nil, layers:[], - }; - LayeredMap.new = func(parent, name) - return make(LayeredMap, parent.createChild("map",name) ); +# TODO: use MapBehavior here and move the zoom/refpos methods there, so that map behavior can be easily customized +var LayeredMap = { + ranges:[], + zoom_property:nil, listeners:[], + update_property:nil, layers:[], +}; +LayeredMap.new = func(parent, name) + return make(LayeredMap, parent.createChild("map",name) ); - LayeredMap.listen = func(p,c) { #FIXME: listening should be managed by each m/v/c separately +LayeredMap.listen = func(p,c) { #FIXME: listening should be managed by each m/v/c separately # print("Setting up LayeredMap-managed listener:", p); append(me.listeners, setlistener(p, c)); - } +} - LayeredMap.initializeLayers = func { +LayeredMap.initializeLayers = func { # print("initializing all layers and updating"); foreach(var l; me.layers) l.update(); - } +} - LayeredMap.setRefPos = func(lat, lon) { - # print("RefPos set"); - me._node.getNode("ref-lat", 1).setDoubleValue(lat); - me._node.getNode("ref-lon", 1).setDoubleValue(lon); - me; # chainable - } - LayeredMap.setHdg = func(hdg) { - me._node.getNode("hdg",1).setDoubleValue(hdg); +LayeredMap.setRefPos = func(lat, lon) { + # print("RefPos set"); + me._node.getNode("ref-lat", 1).setDoubleValue(lat); + me._node.getNode("ref-lon", 1).setDoubleValue(lon); me; # chainable - } +} +LayeredMap.setHdg = func(hdg) { + me._node.getNode("hdg",1).setDoubleValue(hdg); + me; # chainable +} - LayeredMap.updateZoom = func { +LayeredMap.updateZoom = func { var z = me.zoom_property.getValue() or 0; z = math.max(0, math.min(z, size(me.ranges) - 1)); me.zoom_property.setIntValue(z); - var zoom = me.ranges[size(me.ranges) - 1 - z]; + var zoom = me.ranges[size(me.ranges) - 1 - z]; # print("Setting zoom range to:", zoom); - benchmark("Zooming map:"~zoom, func - { + benchmark("Zooming map:"~zoom, func { me._node.getNode("range", 1).setDoubleValue(zoom); # TODO update center/limit translation to keep airport always visible }); me; #chainable - } +} - # this is a huge hack at the moment, we need to encapsulate the setRefPos/setHdg methods, so that they are exposed to XML space - # +# this is a huge hack at the moment, we need to encapsulate the setRefPos/setHdg methods, so that they are exposed to XML space +# LayeredMap.updateState = func { - # center map on airport TODO: should be moved to a method and wrapped with a controller so that behavior can be customizeda - #var apt = me.layers[0]._model._elements[0]; - # FIXME: - #me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon); - - me.setHdg(0.0); - me.updateZoom(); + # center map on airport TODO: should be moved to a method and wrapped with a controller so that behavior can be customized + #var apt = me.layers[0]._model._elements[0]; + # FIXME: + #me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon); + + me.setHdg(0.0); + me.updateZoom(); } # -# TODO: this is currently GUI specific and not re-usable for instruments - LayeredMap.setupZoom = func(dialog) { - var dlgroot = dialog.getNode("features/dialog-root").getValue();#FIXME: GUI specific - needs to be re-implemented for instruments +# TODO: this is currently GUI specific and not re-usable for instruments +LayeredMap.setupZoom = func(dialog) { + var dlgroot = dialog.getNode("features/dialog-root").getValue();#FIXME: GUI specific - needs to be re-implemented for instruments me.zoom_property = props.globals.getNode(dlgroot ~"/"~dialog.getNode("features/range-property").getValue(), 1); #FIXME: this doesn't belong here, need to be in ctor instead !!! ranges=dialog.getNode("features/ranges").getChildren("range"); if( size(me.ranges) == 0 ) @@ -413,132 +278,123 @@ LayeredMap.updateState = func { me.listen(me.zoom_property, func me.updateZoom() ); me.updateZoom(); me; #chainable - } - LayeredMap.setZoom = func {} #TODO - - LayeredMap.resetLayers = func { - - benchmark("Resetting LayeredMap", func - foreach(var l; me.layers) { #TODO: hide all layers, hide map - l.reset(); - } - ); - +} +LayeredMap.setZoom = func {} #TODO +LayeredMap.resetLayers = func { + benchmark("Resetting LayeredMap", + func foreach(var l; me.layers) { #TODO: hide all layers, hide map + l.reset(); + } + ); } - #FIXME: listener management should be done at the MVC level, for each component - not as part of the LayeredMap! - LayeredMap.cleanup_listeners = func { - # print("Cleaning up listeners"); - foreach(var l; me.listeners) - removelistener(l); +#FIXME: listener management should be done at the MVC level, for each component - not as part of the LayeredMap! +LayeredMap.cleanup_listeners = func { + # print("Cleaning up listeners"); + foreach(var l; me.listeners) + removelistener(l); # TODO check why me.listeners = []; doesn't work. Maybe this is a Nasal bug # and the old vector is somehow used again. - setsize(me.listeners, 0); - } + setsize(me.listeners, 0); +} ### -# GenericMap: A generic map is a layered map that puts all supported features on a different layer (canvas group) so that +# GenericMap: A generic map is a layered map that puts all supported features on a different layer (canvas group) so that # they can be individually toggled on/off so that unnecessary updates are avoided, there are methods to link layers to boolean properties # so that they can be easily associated with GUI properties (checkboxes) or cockpit hotspots # TODO: generalize the XML-parametrization and move it to a helper class var GenericMap = { }; - GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap); +GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap); - GenericMap.setupLayer = func(layer, property) { - var l = MAP_LAYERS[layer].new(me, layer); # Layer.new(me, layer); - l.display_layer = property; #FIXME: use controller object instead here and this overlaps with update_property - #print("Set up layer with toggle property=", property); - l._view.setVisible( getprop(property) ) ; - append(me.layers, l); - return l; - } +GenericMap.setupLayer = func(layer, property) { + var l = MAP_LAYERS[layer].new(me, layer,nil); # Layer.new(me, layer); + l.display_layer = property; #FIXME: use controller object instead here and this overlaps with update_property + #print("Set up layer with toggle property=", property); + l._view.setVisible( getprop(property) ) ; + append(me.layers, l); + return l; +} - # features are layers - so this will do layer setup and then register listeners for each layer - GenericMap.setupFeature = func(layer, property, init ) { - var l=me.setupLayer( layer, property ); - me.listen(property, func l.toggle() ); #TODO: should use the controller object here ! +# features are layers - so this will do layer setup and then register listeners for each layer +GenericMap.setupFeature = func(layer, property, init ) { + var l=me.setupLayer( layer, property ); + me.listen(property, func l.toggle() ); #TODO: should use the controller object here ! - l._model._update_property=property; #TODO: move somewhere else - this is the property that is mapped to the CHECKBOX - l._model._view_handle = l; #FIXME: very crude, set a handle to the view(group), so that the model can notify it (for updates) - l._model._map_handle = me; #FIXME: added here so that layers can send update requests to the parent map - #print("Setting up layer init for property:", init); + l._model._update_property=property; #TODO: move somewhere else - this is the property that is mapped to the CHECKBOX + l._model._view = l; #FIXME: very crude, set a handle to the view(group), so that the model can notify it (for updates) + l._model._map = me; #FIXME: added here so that layers can send update requests to the parent map + #print("Setting up layer init for property:", init); - l._model._input_property = init; # FIXME: init property = input property - needs to be improved! - me.listen(init, func l._model.init() ); #TODO: makes sure that the layer's init method for the MODEL is invoked - me; #chainable + l._model._input_property = init; # FIXME: init property = input property - needs to be improved! + me.listen(init, func l._model.init() ); #TODO: makes sure that the layer's init method for the MODEL is invoked + me; #chainable }; - # This will read in the config and procedurally instantiate all requested layers and link them to toggle properties - # FIXME: this is currently GUI specific and doesn't yet support instrument use, i.e. needs to be generalized further - GenericMap.pickupFeatures = func(DIALOG_CANVAS) { - var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue(); - # print("Picking up features for:", DIALOG_CANVAS.getPath() ); - var layers=DIALOG_CANVAS.getNode("features").getChildren("layer"); - foreach(var n; layers) { - var name = n.getNode("name").getValue(); - var toggle = n.getNode("property").getValue(); - var init = n.getNode("init-property").getValue(); - init = dlgroot ~"/"~init; - var property = dlgroot ~"/"~toggle; - # print("Adding layer:",n.getNode("name").getValue() ); - me.setupFeature(name, property, init); - } - me; +# This will read in the config and procedurally instantiate all requested layers and link them to toggle properties +# FIXME: this is currently GUI specific and doesn't yet support instrument use, i.e. needs to be generalized further +GenericMap.pickupFeatures = func(DIALOG_CANVAS) { + var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue(); + # print("Picking up features for:", DIALOG_CANVAS.getPath() ); + var layers=DIALOG_CANVAS.getNode("features").getChildren("layer"); + foreach(var n; layers) { + var name = n.getNode("name").getValue(); + var toggle = n.getNode("property").getValue(); + var init = n.getNode("init-property").getValue(); + init = dlgroot ~"/"~init; + var property = dlgroot ~"/"~toggle; + # print("Adding layer:",n.getNode("name").getValue() ); + me.setupFeature(name, property, init); + } + me; #chainable } # NOT a method, cmdarg() is no longer meaningful when the canvas nasal block is executed # so this needs to be called in the dialog's OPEN block instead - TODO: generalize - #FIXME: move somewhere else, this is a GUI helper and should probably be generalized and moved to gui.nas + #FIXME: move somewhere else, this really is a GUI helper and should probably be generalized and moved to gui.nas GenericMap.setupGUI = func (dialog, group) { - var group = globals.gui.findElementByName(cmdarg() , group); + var group = globals.gui.findElementByName(cmdarg() , group); - var layers=dialog.getNode("features").getChildren("layer"); - var template = dialog.getNode("checkbox-toggle-template"); - var dlgroot = dialog.getNode("features/dialog-root").getValue(); - var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue(); - var i=0; - foreach(var n; layers) { - var name = n.getNode("name").getValue(); - var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue(); - var label = n.getNode("description",1).getValue() or name; + var layers=dialog.getNode("features").getChildren("layer"); + var template = dialog.getNode("checkbox-toggle-template"); + var dlgroot = dialog.getNode("features/dialog-root").getValue(); + var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue(); + var i=0; + foreach(var n; layers) { + var name = n.getNode("name").getValue(); + var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue(); + var label = n.getNode("description",1).getValue() or name; + #var query_range = n.getNode("nav-query-range-property").getValue(); + #print("Query Range:", query_range); - var default = n.getNode("default",1).getValue(); - default = (default=="enabled")?1:0; - #print("Layer default for", name ," is:", default); - setprop(toggle, default); # set the checkbox to its default setting + var default = n.getNode("default",1).getValue(); + default = (default=="enabled")?1:0; + #print("Layer default for", name ," is:", default); + setprop(toggle, default); # set the checkbox to its default setting - var hide_checkbox = n.getNode("hide-checkbox",1).getValue(); - hide_checkbox = (hide_checkbox=="true")?1:0; + var hide_checkbox = n.getNode("hide-checkbox",1).getValue(); + hide_checkbox = (hide_checkbox=="true")?1:0; - var checkbox = group.getChild("checkbox",i, 1); #FIXME: compute proper offset dynamically, will currently overwrite other existing checkboxes! + var checkbox = group.getChild("checkbox",i, 1); #FIXME: compute proper offset dynamically, will currently overwrite other existing checkboxes! - props.copy(template, checkbox); - checkbox.getNode("name").setValue("display-"~name); - checkbox.getNode("label").setValue(label); - checkbox.getNode("property").setValue(toggle); - checkbox.getNode("binding/object-name").setValue("display-"~name); - checkbox.getNode("enabled",1).setValue(!hide_checkbox); - i+=1; - } + props.copy(template, checkbox); + checkbox.getNode("name").setValue("display-"~name); + checkbox.getNode("label").setValue(label); + checkbox.getNode("property").setValue(toggle); + checkbox.getNode("binding/object-name").setValue("display-"~name); + checkbox.getNode("enabled",1).setValue(!hide_checkbox); + i+=1; + } - #add zoom buttons procedurally: - var template = dialog.getNode("zoom-template"); + #now add zoom buttons procedurally: + var template = dialog.getNode("zoom-template"); template.getNode("button[0]/binding[0]/property[0]").setValue(zoom); template.getNode("text[0]/property[0]").setValue(zoom); template.getNode("button[1]/binding[0]/property[0]").setValue(zoom); - template.getNode("button[1]/binding[0]/max[0]").setValue( i ); + template.getNode("button[1]/binding[0]/max[0]").setValue( i ); props.copy(template, group); - } - -### -# TODO: StylableGenericMap (colors, fonts, symbols) -# - -var AirportMap = {}; - AirportMap.new = func(parent,name) make(GenericMap.new(parent,name), AirportMap); - #TODO: Use real MVC (DataProvider/PositionedProvider) here +} # this is currently "directly" invoked via a listener, needs to be changed @@ -546,48 +402,37 @@ var AirportMap = {}; # TODO: adopt real MVC here # FIXME: this must currently be explicitly called by the model, we need to use a wrapper to call it automatically instead! LayerModel.notifyView = func () { - # print("View notified"); - me._view_handle.update(); # update the layer/group - me._map_handle.updateState(); # update the map + # print("View notified"); + me._view.update(); # update the layer/group + + ### UGLY: disabled for now (probably breaks airport GUI dialog !) + ### me._map.updateState(); # update the map } -# ID -var SingleAirportProvider = {}; - -# inputs: position, range -var MultiAirportProvider = {}; - -#TODO: remove and unify with update() -AirportMap.init = func { - me.resetLayers(); - me.updateState(); -} - -# MultiObjectLayer: -# - Airports -# - Traffic (MP/AI) -# - Navaids -# - # TODO: a "MapLayer" is a full MVC implementation that is owned by a "LayeredMap" var MAP_LAYERS = {}; var register_layer = func(name, layer) MAP_LAYERS[name]=layer; var MVC_FOLDER = getprop("/sim/fg-root") ~ "/Nasal/canvas/map/"; -var load_modules = func(vec) foreach(var file; vec) io.load_nasal(MVC_FOLDER~file, "canvas"); +var load_modules = func(vec, ns='canvas') + foreach(var file; vec) + io.load_nasal(MVC_FOLDER~file, ns); # TODO: should probably be using a different/sub-namespace! -# TODO: read in the file names dynamically: *.draw, *.model, *.layer +# read in the file names dynamically: *.draw, *.model, *.layer +var files_with = func(ext) { + var results = []; + var all_files = directory(MVC_FOLDER); + foreach(var file; all_files) { + if(substr(file, -size(ext)) != ext) continue; + append(results, file); + } + return results; +} +foreach(var ext; var extensions = ['.draw','.model','.layer']) + load_modules(files_with(ext)); -var DRAWABLES = ["navaid.draw", "parking.draw", "runways.draw", "taxiways.draw", "tower.draw"]; -load_modules(DRAWABLES); -var MODELS = ["airports.model", "navaids.model",]; -load_modules(MODELS); - -var LAYERS = ["runways.layer", "taxiways.layer", "parking.layer", "tower.layer", "navaids.layer","test.layer",]; -load_modules(LAYERS); - -#TODO: Implement! -var CONTROLLERS = []; -load_modules(CONTROLLERS); +# canvas.MFD = {EFIS:}; # where we'll be storing all MFDs +# TODO: should be inside a separate subfolder, i.e. canvas/map/mfd +load_modules( files_with('.mfd'), 'canvas' ); diff --git a/Nasal/canvas/map/VOR.lcontroller b/Nasal/canvas/map/VOR.lcontroller new file mode 100644 index 000000000..1f63e4e67 --- /dev/null +++ b/Nasal/canvas/map/VOR.lcontroller @@ -0,0 +1,52 @@ +# Class things: +var parents = [SymbolLayer.Controller]; +var __self__ = caller(0)[0]; +SymbolLayer.Controller.add("VOR", __self__); +SymbolLayer.add("VOR", { + parents: [SymbolLayer], + type: "VOR", # Symbol type + df_controller: __self__, # controller to use by default -- this one +}); +var new = func(layer) { + var m = { + parents: [__self__], + layer: layer, + active_vors: [], + navNs: props.globals.getNode("instrumentation").getChildren("nav"), + listeners: [], + }; + setsize(m.active_vors, size(m.navNs)); + foreach (var navN; m.navNs) { + append(m.listeners, setlistener( + navN.getNode("frequencies/selected-mhz"), + func m.changed_freq() + )); + } + #call(debug.dump, keys(layer)); + m.changed_freq(update:0); + return m; +}; +var del = func() { + #print("VOR.lcontroller.del()"); + foreach (var l; me.listeners) + removelistener(l); +}; + +# Controller methods +var isActive = func(model) { + var my_freq = model.frequency/100; + foreach (var freq; me.active_vors) + if (freq == my_freq) return 1; + return 0; +}; +var changed_freq = func(update=1) { + #debug.dump(me.active_vors); + foreach (var navN; me.navNs) + me.active_vors[ navN.getIndex() ] = navN.getValue("frequencies/selected-mhz"); + if (update) me.layer.update(); +}; +var searchCmd = func { + #print("Run query"); + return positioned.findWithinRange(100, 'vor'); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch +}; + diff --git a/Nasal/canvas/map/VOR.scontroller b/Nasal/canvas/map/VOR.scontroller new file mode 100644 index 000000000..b95bcc4f2 --- /dev/null +++ b/Nasal/canvas/map/VOR.scontroller @@ -0,0 +1,10 @@ +# Class things: +var parents = [Symbol.Controller]; +var __self__ = caller(0)[0]; +Symbol.registry["VOR"].df_controller = __self__; +var new = func(model) ; # this controller doesn't need an instance +var LayerController = SymbolLayer.registry["VOR"]; +var isActive = func(model) LayerController.isActive(model); +var query_range = func() + die("VOR.scontroller.query_range /MUST/ be provided by implementation"); + diff --git a/Nasal/canvas/map/VOR.symbol b/Nasal/canvas/map/VOR.symbol new file mode 100644 index 000000000..aaa259dcc --- /dev/null +++ b/Nasal/canvas/map/VOR.symbol @@ -0,0 +1,52 @@ +# Read by the DotSym.readinstance; each variable becomes a derived class's member/method + +var element_type = "group"; # we want a group, becomes "me.element" +var inited = 0; # this allows us to track whether draw() is an init() or an update() +var range_vor = nil; # two elements that get drawn when needed +var radial_vor = nil; # if one is nil, the other has to be nil + +var draw = func { + if (me.inited) { + # Update + if (me.controller.isActive(me.model)) { + if (me.range_vor == nil) { + var rangeNm = me.controller.query_range(); + # print("VOR is tuned:", me.model.id); + var radius = (me.model.range_nm/rangeNm)*345; + me.range_vor = me.element.createChild("path") + .moveTo(-radius,0) + .arcSmallCW(radius,radius,0,2*radius,0) + .arcSmallCW(radius,radius,0,-2*radius,0) + .setStrokeLineWidth(3) + .setStrokeDashArray([5, 15, 5, 15, 5]) + .setColor(0,1,0); + + var course = controller.get_tuned_course(me.model.frequency/100); + vor_grp.createChild("path") + .moveTo(0,-radius) + .vert(2*radius) + .setStrokeLineWidth(3) + .setStrokeDashArray([15, 5, 15, 5, 15]) + .setColor(0,1,0) + .setRotation(course*D2R); + icon_vor.setColor(0,1,0); + } + me.range_vor.show(); + me.radial_vor.show(); + } else { + me.range_vor.hide(); + me.radial_vor.hide(); + } + } else # Init + me.element.createChild("path") + .moveTo(-15,0) + .lineTo(-7.5,12.5) + .lineTo(7.5,12.5) + .lineTo(15,0) + .lineTo(7.5,-12.5) + .lineTo(-7.5,-12.5) + .close() + .setStrokeLineWidth(3) + .setColor(0,0.6,0.85); +}; + diff --git a/Nasal/canvas/map/aircraftpos.controller b/Nasal/canvas/map/aircraftpos.controller new file mode 100644 index 000000000..7e1ead74f --- /dev/null +++ b/Nasal/canvas/map/aircraftpos.controller @@ -0,0 +1,41 @@ +# Class things: +var parents = [Map.Controller]; +var __self__ = caller(0)[0]; +Map.Controller.add("Aircraft position", __self__); +Map.df_controller = __self__; +var new = func(map) { + var m = { + parents: [__self__], + map: map, + _pos: nil, _time: nil, + }; + m.timer = maketimer(0, m, update_pos); + m.timer.start(); + return m; +}; +var del = func(map) { + if (map != me.map) die(); + me.timer.stop(); +}; + +# Controller methods +var update_pos = func { + var (lat,lon) = (var pos = geo.aircraft_position()).latlon(); + var time = systime(); + me.map.setPos(lat, lon, getprop("/orientation/heading-deg")); + if (me._pos == nil) + me._pos = geo.Coord.new(pos); + else { + var dist = me._pos.direct_distance_to(pos); + # 2 NM until we update again + if (dist < 2 * NM2M) return; + # Update at most every 4 seconds to avoid escessive stutter: + elsif (time - me._time < 4) return; + } + #print("update aircraft position"); + var (x,y,z) = pos.xyz(); + me._pos.set_xyz(x,y,z); + me._time = time; + me.map.update(); +}; + diff --git a/Nasal/canvas/map/airport-nd.draw b/Nasal/canvas/map/airport-nd.draw new file mode 100644 index 000000000..9898c45d8 --- /dev/null +++ b/Nasal/canvas/map/airport-nd.draw @@ -0,0 +1,33 @@ +## +# draws a single airport (ND style) +# +var draw_apt = func (group, apt, controller=nil, lod=0) { + var lat = apt.lat; + var lon = apt.lon; + var name = apt.id; + # print("drawing nd airport:", name); + + var apt_grp = group.createChild("group", name); + + # FIXME: conditions don't belong here, use the controller hash instead! + # if (1 or getprop("instrumentation/efis/inputs/arpt")) { + var icon_apt = apt_grp.createChild("path", name ~ " icon" ) + .moveTo(-17,0) + .arcSmallCW(17,17,0,34,0) + .arcSmallCW(17,17,0,-34,0) + .close() + .setColor(0,0.6,0.85) + .setStrokeLineWidth(3); + var text_apt = apt_grp.createChild("text", name ~ " label") + .setDrawMode( canvas.Text.TEXT ) + .setTranslation(17,35) + .setText(name) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") + .setColor(0,0.6,0.85) + .setFontSize(28); + apt_grp.setGeoPosition(lat, lon) + .set("z-index",1); # FIXME: this needs to be configurable!! + #} + + # draw routines should always return their canvas group to the caller for further processing +} diff --git a/Nasal/canvas/map/airports-nd.layer b/Nasal/canvas/map/airports-nd.layer new file mode 100644 index 000000000..09d3d0f00 --- /dev/null +++ b/Nasal/canvas/map/airports-nd.layer @@ -0,0 +1,11 @@ +var AirportsNDLayer = {}; +AirportsNDLayer.new = func(group, name) { + var m = Layer.new(group, name, AirportsNDModel ); + m.setDraw(func draw_layer(layer:m, callback:draw_apt, lod:0) ); + return m; +} + +## +# airport-nd lookup key +register_layer("airports-nd", AirportsNDLayer); + diff --git a/Nasal/canvas/map/airports-nd.model b/Nasal/canvas/map/airports-nd.model new file mode 100644 index 000000000..16c06387d --- /dev/null +++ b/Nasal/canvas/map/airports-nd.model @@ -0,0 +1,38 @@ +var AirportsNDModel = {}; +AirportsNDModel.new = func make(AirportsNDModel, LayerModel); + +AirportsNDModel.init = func { + #print("Updating AirportsNDModel"); + + me._view.reset(); + + var results = positioned.findWithinRange(me._controller.query_range()*2, "airport"); + var numResults = 0; + foreach(result; results) { + if (numResults < 50) { + var apt = airportinfo(result.id); + var runways = apt.runways; + var runway_keys = sort(keys(runways),string.icmp); + var validApt = 0; + foreach(var rwy; runway_keys){ + var r = runways[rwy]; + if (r.length > 1890) # Only display suitably large airports + validApt = 1; + if (result.id == getprop("autopilot/route-manager/destination/airport") or result.id == getprop("autopilot/route-manager/departure/airport")) + validApt = 1; + } + + if(validApt) { + #canvas.draw_apt(me.apt_group, result.lat,result.lon,result.id,i); + me.push(result); + numResults += 1; + } + } + } + # set RefPos and hdg to apt !! + # me._map_handle.setRefPos(apt.lat, apt.lon); + + #TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc) + me.notifyView(); +} + diff --git a/Nasal/canvas/map/airports.model b/Nasal/canvas/map/airports.model index 5dc35e47c..348de7ffd 100644 --- a/Nasal/canvas/map/airports.model +++ b/Nasal/canvas/map/airports.model @@ -1,27 +1,26 @@ var AirportModel = {}; - AirportModel.new = func make(AirportModel, LayerModel); +AirportModel.new = func make(AirportModel, LayerModel); # FIXME: Just testing for now: This really shouldn't be part of the core LayerModel, needs to go to "AirportModel" instead # FIXME: This will get called ONCE for EACH layer that uses the AirportModel, so VERY inefficient ATM! => should be shared among layers + AirportModel.init = func { - # print("AirportModel initialized!"); - # me._map_handle.resetLayers(); - me._view_handle.reset(); - var id = getprop(me._input_property); # HACK: this needs to be handled via the controller - introduce "input_property" - #print("ID is:", id); - (id == "") and return; - var apt=airportinfo(id); # FIXME: replace with controller call to update the model - #var airports = findAirportsWithinRange(apt.lat, apt.lon, 10); # HACK: expose the range !! - foreach(var a; [ apt ]) #FIXME: move to separate method: "populate" - # print("storing:", a.id) and + me._view.reset(); + var id = getprop(me._input_property); # HACK: this needs to be handled via the controller - introduce "input_property" + #print("ID is:", id); + (id == "") and return; + var apt=airportinfo(id); # FIXME: replace with controller call to update the model + + foreach(var a; [ apt ]) #FIXME: move to separate method: "populate" + # print("storing:", a.id) and me.push(a); - #print("Work items in Model:", me.hasData() ); - #print("Model updated!!"); + #print("Work items in Model:", me.hasData() ); + #print("Model updated!!"); - # set RefPos and hdg to apt !! - me._map_handle.setRefPos(apt.lat, apt.lon); + # set RefPos and hdg to apt !! + me._map.setRefPos(apt.lat, apt.lon); - #TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc) - me.notifyView(); + #TODO: Notify view on update - use proper NOTIFICATIONS (INIT; UPDATE etc) + me.notifyView(); } diff --git a/Nasal/canvas/map/altitude-arc.draw b/Nasal/canvas/map/altitude-arc.draw new file mode 100644 index 000000000..7cbc446d1 --- /dev/null +++ b/Nasal/canvas/map/altitude-arc.draw @@ -0,0 +1,32 @@ +## +# pseudo draw routine - rangeNm is passed by the model +# needs further work once we adopt the MapStructure framwork +# HOWEVER, the alt-arc is an exception in that it's not rendered as +# a map item ... + +var draw_altarc = func (group, rangeNm, controller=nil, lod = 0) { +# print("drawing alt-arc, range:", rangeNm); + +var altArc = group.createChild("path","alt-arc") + .setStrokeLineWidth(3) + .setColor(0,1,0) + .set("clip", "rect(124, 1024, 1024, 0)"); + + +if (abs(getprop("velocities/vertical-speed-fps")) > 10) { + altArc.reset(); + + var altRangeNm = (getprop("autopilot/settings/target-altitude-ft")- + getprop("instrumentation/altimeter/indicated-altitude-ft"))/getprop("velocities/vertical-speed-fps")* + getprop("/velocities/groundspeed-kt")*KT2MPS*M2NM; + + if(altRangeNm > 1) { + var altRangePx = (256/rangeNm )*altRangeNm; + altArc.moveTo(-altRangePx*2.25,0) + .arcSmallCW(altRangePx*2.25,altRangePx*2.25,0,altRangePx*4.5,0) + .setTranslation(512,824); + } # altRangeNm > 1 + } # fps > 10 + + +} # draw_altarc diff --git a/Nasal/canvas/map/altitude-arc.layer b/Nasal/canvas/map/altitude-arc.layer new file mode 100644 index 000000000..cddc94ef3 --- /dev/null +++ b/Nasal/canvas/map/altitude-arc.layer @@ -0,0 +1,12 @@ +var AltitudeArcLayer = {}; +AltitudeArcLayer.new = func(group, name, controller=nil) { + var m = Layer.new(group, name, AltitudeArcModel ); + m._model._controller = controller; + m.setDraw(func draw_layer(layer:m, callback:canvas.draw_altarc, lod:0) ); + return m; +} + +## +# airport-nd lookup key +register_layer("altitude-arc", AltitudeArcLayer); + diff --git a/Nasal/canvas/map/altitude-arc.model b/Nasal/canvas/map/altitude-arc.model new file mode 100644 index 000000000..663a40973 --- /dev/null +++ b/Nasal/canvas/map/altitude-arc.model @@ -0,0 +1,19 @@ +var AltitudeArcModel = {}; + AltitudeArcModel.new = func make( LayerModel, AltitudeArcModel ); + + AltitudeArcModel.init = func { +# print("Updating alt-arc model"); +var rangeNm = me._controller['query_range'](); + +me._view.reset(); +me.push(rangeNm); +me.notifyView(); + +## +# FIXME this should be configurable via the controller +# and it should only be running if the predicate (altitude check) is true +# for now it'll suffice though +settimer(func me.init(), 0.3); +} + + diff --git a/Nasal/canvas/map/dme.draw b/Nasal/canvas/map/dme.draw new file mode 100644 index 000000000..07e5e7b54 --- /dev/null +++ b/Nasal/canvas/map/dme.draw @@ -0,0 +1,34 @@ +### +# +# + +var draw_dme = func (group, dme, controller=nil, lod=0) { + var lat = dme.lat; + var lon = dme.lon; + var name = dme.id; + var freq = dme.frequency; + + var dme_grp = group.createChild("group","dme"); + var icon_dme = dme_grp .createChild("path", name) + .moveTo(-15,0) + .line(-12.5,-7.5) + .line(7.5,-12.5) + .line(12.5,7.5) + .lineTo(7.5,-12.5) + .line(12.5,-7.5) + .line(7.5,12.5) + .line(-12.5,7.5) + .lineTo(15,0) + .lineTo(7.5,12.5) + .vert(14.5) + .horiz(-14.5) + .vert(-14.5) + .close() + .setStrokeLineWidth(3) + .setColor(0,0.6,0.85); + dme_grp.setGeoPosition(lat, lon) + .set("z-index",3); + + if( controller != nil and controller['is_tuned'](freq)) + icon_dme.setColor(0,1,0); +} diff --git a/Nasal/canvas/map/dme.layer b/Nasal/canvas/map/dme.layer new file mode 100644 index 000000000..b9c00d624 --- /dev/null +++ b/Nasal/canvas/map/dme.layer @@ -0,0 +1,9 @@ +var DMELayer = {}; +DMELayer.new = func(group,name,controller=nil) { + var m=Layer.new(group, name, DMEModel, controller); + m.setDraw (func draw_layer(layer:m, callback: draw_dme, lod:0) ); + return m; +} + +register_layer("dme", DMELayer); + diff --git a/Nasal/canvas/map/dme.model b/Nasal/canvas/map/dme.model new file mode 100644 index 000000000..90853158d --- /dev/null +++ b/Nasal/canvas/map/dme.model @@ -0,0 +1,22 @@ +var DMEModel = {}; +DMEModel.new = func make( LayerModel, DMEModel ); + +DMEModel.init = func { + me._view.reset(); + + if(0) { + debug.dump( me._controller ); + print(typeof( + me._controller.query_range() + )); + } + + var results = positioned.findWithinRange(me._controller.query_range()*2 ,"dme"); + foreach(result; results) { + me.push(result); + } + + me.notifyView(); +} + + diff --git a/Nasal/canvas/map/fix.draw b/Nasal/canvas/map/fix.draw new file mode 100644 index 000000000..db2c8c9e6 --- /dev/null +++ b/Nasal/canvas/map/fix.draw @@ -0,0 +1,33 @@ +## +# draw a single fix symbol +# + +var draw_fix = func (group, fix, controller=nil, lod=0) { + var lat = fix.lat; + var lon = fix.lon; + var name = fix.id; + + var fix_grp = group.createChild("group",'fix-'~name); # one group for each fix + + # the fix symbol + var icon_fix = fix_grp.createChild("path", "fix-icon-"~ name) + .moveTo(-15,15) + .lineTo(0,-15) + .lineTo(15,15) + .close() + .setStrokeLineWidth(3) + .setColor(0,0.6,0.85); + + # the fix label + var text_fix = fix_grp.createChild("text", 'fix-label-'~name) + .setDrawMode( canvas.Text.TEXT ) + .setText(name) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") + .setFontSize(28) + .setTranslation(5,25); + + # the fix position + fix_grp.setGeoPosition(lat, lon) + .set("z-index",3); + +} diff --git a/Nasal/canvas/map/fixes.layer b/Nasal/canvas/map/fixes.layer new file mode 100644 index 000000000..381d68bd4 --- /dev/null +++ b/Nasal/canvas/map/fixes.layer @@ -0,0 +1,12 @@ +var FixLayer = {}; +FixLayer.new = func(group,name, controller) { + var m=Layer.new(group, name, FixModel); + #print("new fix layer, dumping controller:"); + #debug.dump(controller); + m._model._controller = controller; # set up the controller for the model !!!!! + m.setDraw (func draw_layer(layer:m, callback: draw_fix, lod:0) ); + return m; +} + +register_layer("fixes", FixLayer); + diff --git a/Nasal/canvas/map/fixes.model b/Nasal/canvas/map/fixes.model index e69de29bb..a07b01351 100644 --- a/Nasal/canvas/map/fixes.model +++ b/Nasal/canvas/map/fixes.model @@ -0,0 +1,26 @@ +var FixModel = {}; +FixModel.new = func make( LayerModel, FixModel ); + +FixModel.init = func { + me._view.reset(); # wraps removeAllChildren() ATM + + #fgcommand('profiler-start'); + #me._view._view.removeAllChildren(); # clear the "real" canvas drawables + #fgcommand('profiler-stop'); + #me.clear(); + + #debug.dump( me._controller) ; + #print("Query range is:", me._controller['query_range']() ); + + var results = positioned.findWithinRange( me._controller['query_range']()*2 ,"fix"); + foreach(result; results) { + me.push(result); + } + + #print("query range was:", me._controller['query_range']()*2); + #print("total fixes in results/model:", size(results)); + + me.notifyView(); +} + + diff --git a/Nasal/canvas/map/navaid.draw b/Nasal/canvas/map/navaid.draw index c053e76ef..7e99ce833 100644 --- a/Nasal/canvas/map/navaid.draw +++ b/Nasal/canvas/map/navaid.draw @@ -2,14 +2,15 @@ # FIXME: until we have better instancing support for symbols, it would be better to return a functor here # so that symbols are only parsed once var NAVAID_CACHE = {}; + var draw_navaid = func (group, navaid, lod) { - #var group = group.createChild("group", "navaid"); - DEBUG and print("Drawing navaid:", navaid.id); - var symbols = {NDB:"/gui/dialogs/images/ndb_symbol.svg"}; # TODO: add more navaid symbols here - if (symbols[navaid.type] == nil) return print("Missing svg image for navaid:", navaid.type); - - var symbol_navaid = group.createChild("group", "navaid"); - canvas.parsesvg(symbol_navaid, symbols[navaid.type]); - symbol_navaid.setGeoPosition(navaid.lat, navaid.lon); + #var group = group.createChild("group", "navaid"); + DEBUG and print("Drawing navaid:", navaid.id); + var symbols = {NDB:"/gui/dialogs/images/ndb_symbol.svg"}; # TODO: add more navaid symbols here + if (symbols[navaid.type] == nil) return print("Missing svg image for navaid:", navaid.type); + + var symbol_navaid = group.createChild("group", "navaid"); + canvas.parsesvg(symbol_navaid, symbols[navaid.type]); + symbol_navaid.setGeoPosition(navaid.lat, navaid.lon); } diff --git a/Nasal/canvas/map/navaids.layer b/Nasal/canvas/map/navaids.layer index 8bc1761d6..dbe61311a 100644 --- a/Nasal/canvas/map/navaids.layer +++ b/Nasal/canvas/map/navaids.layer @@ -1,8 +1,8 @@ var NavLayer = {}; - NavLayer.new = func(group,name) { - var m=Layer.new(group, name, NavaidModel); - m.setDraw (func draw_layer(layer:m, callback: draw_navaid, lod:0) ); - return m; +NavLayer.new = func(group,name) { + var m=Layer.new(group, name, NavaidModel); + m.setDraw (func draw_layer(layer:m, callback: draw_navaid, lod:0) ); + return m; } register_layer("navaids", NavLayer); diff --git a/Nasal/canvas/map/navaids.model b/Nasal/canvas/map/navaids.model index 2f5633525..09e200453 100644 --- a/Nasal/canvas/map/navaids.model +++ b/Nasal/canvas/map/navaids.model @@ -1,11 +1,11 @@ var NavaidModel = {}; - NavaidModel.new = func make(LayerModel, NavaidModel); - NavaidModel.init = func { - me._view_handle.reset(); - var navaids = findNavaidsWithinRange(15); - foreach(var n; navaids) - me.push(n); - me.notifyView(); +NavaidModel.new = func make(LayerModel, NavaidModel); +NavaidModel.init = func { + me._view.reset(); + var navaids = findNavaidsWithinRange(15); + foreach(var n; navaids) + me.push(n); + me.notifyView(); } diff --git a/Nasal/canvas/map/navdisplay.mfd b/Nasal/canvas/map/navdisplay.mfd new file mode 100644 index 000000000..cf3fe8161 --- /dev/null +++ b/Nasal/canvas/map/navdisplay.mfd @@ -0,0 +1,648 @@ +# ============================================================================== +# Boeing Navigation Display by Gijs de Rooy (currently specific to the 744) +# ============================================================================== + + +## +# do we really need to keep track of each drawable here ?? +var i = 0; + + + + +## +# pseudo DSL-ish: use these as placeholders in the config hash below +var ALWAYS = func 1; +var NOTHING = func nil; + +## +# so that we only need to update a single line ... +# +var trigger_update = func(layer) layer._model.init(); + +## +# TODO: move ND-specific implementation details into this lookup hash +# so that other aircraft and ND types can be more easily supported +# +# any aircraft-specific ND behavior should be wrapped here, +# to isolate/decouple things in the generic NavDisplay class +# +# Note to Gijs: this may look weird and confusing, but it' actually requires +# less coding now, and it is now even possible to configure things via a little +# XML wrapper +# TODO: move this to an XML config file +# +var NDStyles = { + ## + # this configures the 744 ND to help generalize the NavDisplay class itself + 'B747-400': { + font_mapper: func(family, weight) { + if( family == "Liberation Sans" and weight == "normal" ) + return "LiberationFonts/LiberationSans-Regular.ttf"; + }, + + # where all the symbols are stored + # TODO: the SVG image should be moved to the canvas folder + # so that it can be shared by other aircraft, i.e. no 744 dependencies + # also SVG elements should be renamed to use boeing/airbus prefix + # aircraft developers should all be editing the same ND.svg image + # the code can deal with the differences now + svg_filename: "Aircraft/747-400/Models/Cockpit/Instruments/ND/ND.svg", + +## +## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map) +## + + layers: [ + { name:'fixes', update_on:['toggle_range','toggle_waypoints'], predicate: func(nd, layer) { + # print("Running fixes predicate"); + var visible=nd.get_switch('toggle_waypoints'); + if(nd.rangeNm() <= 40 and visible and + nd.get_switch('toggle_display_mode') == "MAP") { + # print("fixes update requested!"); + trigger_update( layer ); + } layer._view.setVisible(visible); + + }, # end of layer update predicate + }, # end of fixes layer + + { name:'airports-nd', update_on:['toggle_range','toggle_airports','toggle_display_mode'], predicate: func(nd, layer) { + # print("Running airports-nd predicate"); + if (nd.rangeNm() <= 80 and + nd.get_switch('toggle_display_mode') == "MAP" ) { + trigger_update( layer ); # clear & redraw + } + layer._view.setVisible( nd.get_switch('toggle_airports') ); + + }, # end of layer update predicate + }, # end of airports layer + + { name:'vor', update_on:['toggle_range','toggle_stations','toggle_display_mode'], predicate: func(nd, layer) { + # print("Running vor layer predicate"); + if(nd.rangeNm() <= 40 and + nd.get_switch('toggle_stations') and + nd.get_switch('toggle_display_mode') == "MAP"){ + trigger_update( layer ); # clear & redraw + } + layer._view.setVisible( nd.get_switch('toggle_stations') ); + }, # end of layer update predicate + }, # end of VOR layer + + { name:'dme', update_on:['toggle_range','toggle_stations'], predicate: func(nd, layer) { + if(nd.rangeNm() <= 40 and + nd.get_switch('toggle_stations') and + nd.get_switch('toggle_display_mode') == "MAP"){ + trigger_update( layer ); # clear & redraw + } + layer._view.setVisible( nd.get_switch('toggle_stations') ); + }, # end of layer update predicate + }, # end of DME layer + + { name:'mp-traffic', update_on:['toggle_range','toggle_traffic'], predicate: func(nd, layer) { + trigger_update( layer ); # clear & redraw + layer._view.setVisible( 1 ); #nd.get_switch('toggle_traffic') + }, # end of layer update predicate + }, # end of traffic layer + + + { name:'runway-nd', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { + var visible = (nd.rangeNm() <= 40 and getprop("autopilot/route-manager/active") ) ; + if (visible) + trigger_update( layer ); # clear & redraw + layer._view.setVisible( visible ); + }, # end of layer update predicate + }, # end of airports-nd layer + + { name:'route', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { + trigger_update( layer ); # clear & redraw + layer._view.setVisible( 1 ); #nd.get_switch('toggle_traffic') + }, # end of layer update predicate + }, # end of route layer + + { name:'altitude-arc', not_a_map:1, update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { + trigger_update( layer ); # clear & redraw + layer._view.setVisible( 1 ); + }, # end of layer update predicate + }, # end of alt-arc layer + +## add other layers here, layer names must match the registered names as used in *.layer files for now +## this will all change once we're using Philosopher's MapStructure framework + + ], # end of vector with configured layers + + # This is where SVG elements are configured by providing "behavior" hashes, i.e. for animations + + # to animate each SVG symbol, specify behavior via callbacks (predicate, and true/false implementation) + # SVG identifier, callback etc + # TODO: update_on([]), update_mode (update() vs. timers/listeners) + # TODO: support putting symbols on specific layers + features: [ { + # TODO: taOnly doesn't need to use getprop polling in update(), use a listener instead! + id: 'taOnly', # the SVG ID + impl: { # implementation hash + init: func(nd, symbol), # for updateCenter stuff, called during initialization in the ctor + predicate: func(nd) getprop("instrumentation/tcas/inputs/mode") == 2, # the condition + is_true: func(nd) nd.symbols.taOnly.show(), # if true, run this + is_false: func(nd) nd.symbols.taOnly.hide(), # if false, run this + }, # end of taOnly behavior/callbacks + }, # end of taOnly + { + id: 'tas', + impl: { + init: func(nd,symbol), + predicate: func(nd) getprop("/velocities/airspeed-kt") > 100, + is_true: func(nd) { + nd.symbols.tas.setText(sprintf("%3.0f",getprop("/velocities/airspeed-kt") )); + nd.symbols.tas.show(); + }, + is_false: func(nd) nd.symbols.tas.hide(), + }, # end of tas behavior callbacks + }, # end of tas hash + { + id: 'eta', + impl: { + init: func(nd,symbol), + predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil, + is_true: func(nd) { + var eta=getprop("autopilot/route-manager/wp/eta"); + var etaWp = split(":",eta); + var h = getprop("/sim/time/utc/hour"); + var m = getprop("/sim/time/utc/minute")+sprintf("%02f",etaWp[0]); + var s = getprop("/sim/time/utc/second")+sprintf("%02f",etaWp[1]); + nd.symbols.eta.setText(sprintf("%02.0f%02.0f.%02.0fz",h,m,s)); + nd.symbols.eta.show(); + }, + is_false: func(nd) nd.symbols.eta.hide(), + }, # of eta.impl + }, # of eta + { id:'hdg', + impl: { + init: func(nd,symbol), + predicate: ALWAYS, # always true + is_true: func(nd) nd.symbols.hdg.setText(sprintf("%03.0f", nd.aircraft_source.get_hdg() )), + is_false: NOTHING, + }, # of hdg.impl + }, # of hdg + + { id:'gs', + impl: { + init: func(nd,symbol), + common: func(nd) nd.symbols.gs.setText(sprintf("%3.0f",nd.aircraft_source.get_spd() )), + predicate: func(nd) nd.aircraft_source.get_spd() >= 30, + is_true: func(nd) { + nd.symbols.gs.setFontSize(36); + }, + is_false: func(nd) nd.symbols.gs.setFontSize(52), + }, # of gs.impl + }, # of gs + + { id:'compass', + impl: { + init: func(nd,symbol) nd.getElementById(symbol.id).updateCenter(), + common: func(nd) , + predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','MAP','PLAN','VOR'] )), + is_true: func(nd) { + # # orientation/track-magnetic-deg is noisy + nd.symbols.compass.setRotation(-nd.aircraft_source.get_hdg() *D2R); + nd.symbols.compass.show(); + }, + is_false: func(nd) nd.symbols.compass.hide(), + }, # of compass.impl + }, # of compass + + + + + ], # end of vector with features + + + }, # end of 744 ND style +##### +## +## add support for other aircraft/ND types and styles here (737, 757, 777 etc) +## +## + +}; # end of NDStyles + + +## +# encapsulate hdg/lat/lon source, so that the ND may also display AI/MP aircraft in a pilot-view at some point (aka stress-testing) +# + +var NDSourceDriver = {}; +NDSourceDriver.new = func { + var m = {parents:[NDSourceDriver]}; + m.get_hdg= func getprop("/orientation/heading-deg"); + m.get_lat= func getprop("/position/latitude-deg"); + m.get_lon= func getprop("/position/longitude-deg"); + m.get_spd= func getprop("/velocities/groundspeed-kt"); +return m; +} + + +## +# configure aircraft specific cockpit switches here +# these are some defaults, can be overridden when calling NavDisplay.new() - +# see the 744 ND.nas file the backend code should never deal directly with +# aircraft specific properties using getprop. +# To get started implementing your own ND, just copy the switches hash to your +# ND.nas file and map the keys to your cockpit properties - and things will just work. + +# TODO: switches are ND specific, so move to the NDStyle hash! + +var default_switches = { + 'toggle_range': {path: '/inputs/range-nm', value:40, type:'INT'}, + 'toggle_weather': {path: '/inputs/wxr', value:0, type:'BOOL'}, + 'toggle_airports': {path: '/inputs/arpt', value:0, type:'BOOL'}, + 'toggle_stations': {path: '/inputs/sta', value:0, type:'BOOL'}, + 'toggle_waypoints': {path: '/inputs/wpt', value:0, type:'BOOL'}, + 'toggle_position': {path: '/inputs/pos', value:0, type:'BOOL'}, + 'toggle_data': {path: '/inputs/data',value:0, type:'BOOL'}, + 'toggle_terrain': {path: '/inputs/terr',value:0, type:'BOOL'}, + 'toggle_traffic': {path: '/inputs/tcas',value:0, type:'BOOL'}, + 'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'}, +}; + +## +# TODO: +# - introduce a MFD class (use it also for PFD/EICAS) +# - introduce a SGSubsystem class and use it here +# - introduce a Boeing NavDisplay class +var NavDisplay = { + + # reset handler + handle_reinit: func { + print("Cleaning up NavDisplay listeners"); + # shut down all timers and other loops here + me.update_timer.stop(); + foreach(var l; me.listeners) + removelistener(l); + }, + + listen: func(p,c) { + append(me.listeners, setlistener(p,c)); + }, + + # listeners for cockpit switches + listen_switch: func(s,c) { + # print("event setup for: ", id(c)); + me.listen( me.get_full_switch_path(s), func { + # print("listen_switch triggered:", s, " callback id:", id(c) ); + c(); + }); + + }, + + # get the full property path for a given switch + get_full_switch_path: func (s) { + # debug.dump( me.efis_switches[s] ); + return me.efis_path ~ me.efis_switches[s].path; # FIXME: should be using props.nas instead of ~ + }, + + # helper method for getting configurable cockpit switches (which are usually different in each aircraft) + get_switch: func(s) { + var switch = me.efis_switches[s]; + var path = me.efis_path ~ switch.path ; + #print(s,":Getting switch prop:", path); + + return getprop( path ); + }, + + # for creating NDs that are driven by AI traffic instead of the main aircraft (generalization rocks!) + connectAI: func(source=nil) { + me.aircraft_source = { + get_hdg: func source.getNode('orientation/true-heading-deg').getValue(), + get_lat: func source.getNode('position/latitude-deg').getValue(), + get_lon: func source.getNode('position/longitude-deg').getValue(), + get_spd: func source.getNode('velocities/true-airspeed-kt').getValue(), + }; + }, # of connectAI + + # TODO: the ctor should allow customization, for different aircraft + # especially properties and SVG files/handles (747, 757, 777 etc) + new : func(prop1, switches=default_switches, style='B747-400') { + var m = { parents : [NavDisplay]}; + + m.listeners=[]; # for cleanup handling + m.aircraft_source = NDSourceDriver.new(); # uses the main aircraft as the driver/source (speeds, position, heading) + + m.nd_style = NDStyles[style]; # look up ND specific stuff (file names etc) + + m.radio_list=["instrumentation/comm/frequencies","instrumentation/comm[1]/frequencies", + "instrumentation/nav/frequencies","instrumentation/nav[1]/frequencies"]; + m.mfd_mode_list=["APP","VOR","MAP","PLAN"]; + + m.efis_path = prop1; + m.efis_switches = switches ; + + # just an alias, to avoid having to rewrite the old code for now + m.rangeNm = func m.get_switch('toggle_range'); + + m.efis = props.globals.initNode(prop1); + m.mfd = m.efis.initNode("mfd"); + + # TODO: unify this with switch handling + m.mfd_mode_num = m.mfd.initNode("mode-num",2,"INT"); + m.mfd_display_mode = m.mfd.initNode("display-mode",m.mfd_mode_list[2],"STRING"); + m.std_mode = m.efis.initNode("inputs/setting-std",0,"BOOL"); + m.previous_set = m.efis.initNode("inhg-previos",29.92); # watch out typo here, check other files before fixing ! + m.kpa_mode = m.efis.initNode("inputs/kpa-mode",0,"BOOL"); + m.kpa_output = m.efis.initNode("inhg-kpa",29.92); + m.kpa_prevoutput = m.efis.initNode("inhg-kpa-previous",29.92); + m.temp = m.efis.initNode("fixed-temp",0); + m.alt_meters = m.efis.initNode("inputs/alt-meters",0,"BOOL"); + m.fpv = m.efis.initNode("inputs/fpv",0,"BOOL"); + m.nd_centered = m.efis.initNode("inputs/nd-centered",0,"BOOL"); + + m.mins_mode = m.efis.initNode("inputs/minimums-mode",0,"BOOL"); + m.mins_mode_txt = m.efis.initNode("minimums-mode-text","RADIO","STRING"); + m.minimums = m.efis.initNode("minimums",250,"INT"); + m.mk_minimums = props.globals.getNode("instrumentation/mk-viii/inputs/arinc429/decision-height"); + + # TODO: these are switches, can be unified with switch handling hash above (eventually): + + m.rh_vor_adf = m.efis.initNode("inputs/rh-vor-adf",0,"INT"); # not yet in switches hash + m.lh_vor_adf = m.efis.initNode("inputs/lh-vor-adf",0,"INT"); # not yet in switches hash + m.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # ditto + + ### + # initialize all switches based on the defaults specified in the switch hash + # + foreach(var switch; keys( m.efis_switches ) ) + props.globals.initNode + ( m.get_full_switch_path (switch), + m.efis_switches[switch].value, + m.efis_switches[switch].type + ); + + + return m; + }, + newMFD: func(canvas_group) + { + + me.listen("/sim/signals/reinit", func me.handle_reinit() ); + + me.update_timer = maketimer(0.05, func me.update() ); # TODO: make interval configurable via ctor + me.nd = canvas_group; + + + # load the specified SVG file into the me.nd group and populate all sub groups + + canvas.parsesvg(me.nd, me.nd_style.svg_filename, {'font-mapper': me.nd_style.font_mapper}); + + me.symbols = {}; # storage for SVG elements, to avoid namespace pollution (all SVG elements end up here) + + foreach(var feature; me.nd_style.features ) { + # print("Setting up SVG feature:", feature.id); + me.symbols[feature.id] = me.nd.getElementById(feature.id); + if(contains(feature.impl,'init')) feature.impl.init(me.nd, feature); # call The element's init code (i.e. updateCenter) + } + + ### this is the "old" method that's less flexible, we want to use the style hash instead (see above) + # because things are much better configurable that way + # now look up all required SVG elements and initialize member fields using the same name to have a convenient handle + foreach(var element; ["wpActiveId","wpActiveDist","wind", + "dmeLDist","dmeRDist","vorLId","vorRId", + "range","status.wxr","status.wpt", + "status.sta","status.arpt"]) + me.symbols[element] = me.nd.getElementById(element); + + # load elements from vector image, and create instance variables using identical names, and call updateCenter() on each + # anything that needs updatecenter called, should be added to the vector here + # + foreach(var element; ["rotateComp","windArrow","selHdg", + "curHdgPtr","staFromL","staToL", + "staFromR","staToR"] ) + me.symbols[element] = me.nd.getElementById(element).updateCenter(); + + # this should probably be using Philosopher's new SymbolLayer ? + me.map = me.nd.createChild("map","map") + .setTranslation(512,824) + .set("clip", "rect(124, 1024, 1024, 0)"); + + # this callback will be passed onto the model via the controller hash, and used for the positioned queries, to specify max query range: + + var get_range = func me.get_switch('toggle_range'); + + # predicate for the draw controller + var is_tuned = func(freq) { + var nav1=getprop("instrumentation/nav[0]/frequencies/selected-mhz"); + var nav2=getprop("instrumentation/nav[1]/frequencies/selected-mhz"); + if (freq == nav1 or freq == nav2) return 1; + return 0; + } + + # another predicate for the draw controller + var get_course_by_freq = func(freq) { + if (freq == getprop("instrumentation/nav[0]/frequencies/selected-mhz")) + return getprop("instrumentation/nav[0]/radials/selected-deg"); + else + return getprop("instrumentation/nav[1]/radials/selected-deg"); + } + + var get_current_position = func { + return [ + me.aircraft_source.get_lat(), me.aircraft_source.get_lon() + ]; + } + + # a hash with controller callbacks, will be passed onto draw routines to customize behavior/appearance + # the point being that draw routines don't know anything about their frontends (instrument or GUI dialog) + # so we need some simple way to communicate between frontend<->backend until we have real controllers + # for now, a single controller hash is shared by most layers - unsupported callbacks are simply ignored by the draw files + # + var controller = { query_range: func get_range(), + is_tuned:is_tuned, + get_tuned_course:get_course_by_freq, + get_position: get_current_position, + }; + + ### + # set up various layers, controlled via callbacks in the controller hash + # revisit this code once Philosopher's "Smart MVC Symbols/Layers" work is committed and integrated + + # helper / closure generator + var make_event_handler = func(predicate, layer) func predicate(me, layer); + + me.layers={}; # storage container for all ND specific layers + # look up all required layers as specified per the NDStyle hash and do the initial setup for event handling + + foreach(var layer; me.nd_style.layers) { + print("newMFD(): Setting up ND layer:", layer.name); + # huge hack for the alt-arc, which is not rendered as a map group, but directly as part of the toplevel ND group + var render_target = (!contains(layer,'not_a_map') or !layer.not_a_map) ? me.map : me.nd; + var the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller ); + + # now register all layer specific notification listeners and their corresponding update predicate/callback + # pass the ND instance and the layer handle to the predicate when it is called + # so that it can directly access the ND instance and its own layer (without having to know the layer's name) + + var event_handler = make_event_handler(layer.predicate, the_layer); + foreach(var event; layer.update_on) { + # print("Setting up subscription:", event, " for ", layer.name, " handler id:", id(event_handler) ); + me.listen_switch(event, event_handler ) ; + } # foreach event subscription + # and now update/init each layer once by calling its update predicate for initialization + event_handler(); + } # foreach layer + + print("navdisplay.mfd:ND layer setup completed"); + + # start the update timer, which makes sure that the update() will be called + me.update_timer.start(); + + + # next, radio & autopilot & listeners + # TODO: move this to .init field in layers hash or to model files + foreach(var n; var radios = [ "instrumentation/nav/frequencies/selected-mhz", + "instrumentation/nav[1]/frequencies/selected-mhz"]) + me.listen(n, func() { + me.drawvor(); + me.drawdme(); + }); + # TODO: move this to the route.model + me.listen("/autopilot/route-manager/active", func(active) { + if(active.getValue()) { + me.drawroute(); + me.drawrunways(); + } else { + print("TODO: navdisplay.mfd: implement route-manager/layer clearing!"); + #me.route_group.removeAllChildren(); # HACK! + } + }); + me.listen("/autopilot/route-manager/current-wp", func(activeWp) { + canvas.updatewp( activeWp.getValue() ); + }); + + }, + drawroute: func print("drawroute no longer used!"), + drawrunways: func print("drawrunways no longer used!"), + + in_mode:func(switch, modes) foreach(var m; modes) { + if (me.get_switch(switch)==m) return 1; + else continue; + print("not in checked mode"); + return 0; + }, + + # each model should keep track of when it last got updated, using current lat/lon + # in update(), we can then check if the aircraft has traveled more than 0.5-1 nm (depending on selected range) + # and update each model accordingly + update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft + { + + ## + # important constants + var m1 = 111132.92; + var m2 = -559.82; + var m3 = 1.175; + var m4 = -0.0023; + var p1 = 111412.84; + var p2 = -93.5; + var p3 = 0.118; + var latNm = 60; + var lonNm = 60; + + + # fgcommand('profiler-start'); + + var userHdg = me.aircraft_source.get_hdg(); + var userTrkMag = me.aircraft_source.get_hdg(); # getprop("orientation/heading-deg"); # orientation/track-magnetic-deg is noisy + var userLat = me.aircraft_source.get_lat(); + var userLon = me.aircraft_source.get_lon(); + + # this should only ever happen when testing the experimental AI/MP ND driver hash (not critical) + if (!userHdg or !userTrkMag or !userLat or !userLon) { + print("aircraft source invalid, returning !"); + return; + } + + # Calculate length in NM of one degree at current location TODO: expose as methods, for external callbacks + var userLatR = userLat*D2R; + var userLonR = userLon*D2R; + var latlen = m1 + (m2 * math.cos(2 * userLatR)) + (m3 * math.cos(4 * userLatR)) + (m4 * math.cos(6 * userLatR)); + var lonlen = (p1 * math.cos(userLatR)) + (p2 * math.cos(3 * userLatR)) + (p3 * math.cos(5 * userLatR)); + latNm = latlen*M2NM; #60 at equator + lonNm = lonlen*M2NM; #60 at equator + + me.symbols.windArrow.setRotation((getprop("/environment/wind-from-heading-deg")-userHdg)*D2R); + me.symbols.wind.setText(sprintf("%3.0f / %2.0f",getprop("/environment/wind-from-heading-deg"),getprop("/environment/wind-speed-kt"))); + + + + if ((var navid0=getprop("instrumentation/nav/nav-id"))!=nil ) + me.symbols.vorLId.setText(navid0); + if ((var navid1=getprop("instrumentation/nav[1]/nav-id"))!=nil ) + me.symbols.vorRId.setText(navid1); + if((var nav0dist=getprop("instrumentation/nav/nav-distance"))!=nil ) + me.symbols.dmeLDist.setText(sprintf("%3.1f",nav0dist*0.000539)); + if((var nav1dist=getprop("instrumentation/nav[1]/nav-distance"))!=nil ) + me.symbols.dmeRDist.setText(sprintf("%3.1f",nav1dist*0.000539)); + + me.symbols.range.setText(sprintf("%3.0f",me.rangeNm() )); + #rangeNm=rangeNm*2; + + # updates two SVG symbols, should use a listener specified in the config hash + if(getprop("/autopilot/route-manager/active")) { + me.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id")); + me.symbols.wpActiveDist.setText(sprintf("%3.01fNM",getprop("/autopilot/route-manager/wp/dist"))); + } + + # reposition the map, change heading & range: + me.map._node.getNode("ref-lat",1).setDoubleValue(userLat); + me.map._node.getNode("ref-lon",1).setDoubleValue(userLon); + me.map._node.getNode("hdg",1).setDoubleValue(userHdg); # should also be using a listener for this + me.map._node.getNode("range",1).setDoubleValue(me.rangeNm()/2); # avoid this here, use a listener instead + + + me.symbols.rotateComp.setRotation(-userTrkMag*D2R); + + ## these would require additional arguments to be moved to an external config hash currently + me.symbols.curHdgPtr.setRotation(userHdg*D2R); + me.symbols.selHdg.setRotation(getprop("autopilot/settings/true-heading-deg")*D2R); + if (var nav0hdg=getprop("instrumentation/nav/heading-deg") != nil) + me.symbols.staFromL.setRotation((nav0hdg-userHdg+180)*D2R); + if (var nav0hdg=getprop("instrumentation/nav/heading-deg") != nil) + me.symbols.staToL.setRotation((nav0hdg-userHdg)*D2R); + if (var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") != nil) + me.symbols.staFromR.setRotation((nav1hdg-userHdg+180)*D2R); + if (var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") != nil) + me.symbols.staToR.setRotation((nav1hdg-userHdg)*D2R); + + + ## run all predicates in the NDStyle hash and evaluate their true/false behavior callbacks + ## this is in line with the original design, but normally we don't need to getprop/poll here, + ## using listeners or timers would be more canvas-friendly whenever possible + ## because running setprop() on any group/canvas element at framerate means that the canvas + ## will be updated at frame rate too - wasteful ... (check the performance monitor!) + + foreach(var feature; me.nd_style.features ) { + + # for stuff that always needs to be updated + if (contains(feature.impl, 'common')) feature.impl.common(me); + # conditional stuff + if(!contains(feature.impl, 'predicate')) continue; # no conditional stuff + if ( var result=feature.impl.predicate(me) ) { + # print("Update predicate true for ", feature.id); + feature.impl.is_true(me, result); # pass the result to the predicate + } + else { + # print("Update predicate false for ", feature.id); + feature.impl.is_false( me, result ); # pass the result to the predicate + } + } + + ## update the status flags shown on the ND (wxr, wpt, arpt, sta) + # this could/should be using listeners instead ... + + + me.symbols['status.wxr'].setVisible( me.get_switch('toggle_weather') ); + me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints')); + me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports')); + me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') ); + + + } +}; diff --git a/Nasal/canvas/map/parking.draw b/Nasal/canvas/map/parking.draw index 6f3cc6f97..1d79bd544 100644 --- a/Nasal/canvas/map/parking.draw +++ b/Nasal/canvas/map/parking.draw @@ -1,15 +1,14 @@ var draw_parking = func(group, apt, lod) { - var group = group.createChild("group", "apt-"~apt.id); - foreach(var park; apt.parking()) - { - var icon_park = - group.createChild("text", "parking-" ~ park.name) - .setDrawMode( canvas.Text.ALIGNMENT - + canvas.Text.TEXT ) - .setText(park.name) - .setFont("LiberationFonts/LiberationMono-Bold.ttf") - .setGeoPosition(park.lat, park.lon) - .setFontSize(15, 1.3); - } + var group = group.createChild("group", "apt-"~apt.id); + foreach(var park; apt.parking()) { + var icon_park = + group.createChild("text", "parking-" ~ park.name) + .setDrawMode( canvas.Text.ALIGNMENT + + canvas.Text.TEXT ) + .setText(park.name) + .setFont("LiberationFonts/LiberationMono-Bold.ttf") + .setGeoPosition(park.lat, park.lon) + .setFontSize(15, 1.3); + } } diff --git a/Nasal/canvas/map/parking.layer b/Nasal/canvas/map/parking.layer index 1b6ef1f02..052448711 100644 --- a/Nasal/canvas/map/parking.layer +++ b/Nasal/canvas/map/parking.layer @@ -1,9 +1,9 @@ #TODO: use custom Model/DataProvider var ParkingLayer = {}; # make(Layer); - ParkingLayer.new = func(group, name) { - var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! - m.setDraw( func draw_layer(layer: m, callback: draw_parking, lod:0 ) ); - return m; +ParkingLayer.new = func(group, name) { + var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! + m.setDraw( func draw_layer(layer: m, callback: draw_parking, lod:0 ) ); + return m; } register_layer("parkings", ParkingLayer); diff --git a/Nasal/canvas/map/route.draw b/Nasal/canvas/map/route.draw new file mode 100644 index 000000000..13418892a --- /dev/null +++ b/Nasal/canvas/map/route.draw @@ -0,0 +1,68 @@ +## +# Draw a route with tracks and waypoints (from Gijs' 744 ND.nas code) +# + + +## FIXME: encapsulate properly +var wp = []; +var text_wp = []; + + # Change color of active waypoints + var updatewp = func(activeWp) + { + forindex(var i; wp) { + if(i == activeWp) { + wp[i].setColor(1,0,1); + #text_wp[i].setColor(1,0,1); + } else { + wp[i].setColor(1,1,1); + #text_wp[i].setColor(1,1,1); + } + } + }; + + +var draw_route = func (group, theroute, controller=nil, lod=0) + { + #print("draw_route"); + var route_group = group; + + var route = route_group.createChild("path","route") + .setStrokeLineWidth(5) + .setColor(1,0,1); + + var cmds = []; + var coords = []; + + var fp = flightplan(); + var fpSize = fp.getPlanSize(); + + wp = []; + text_wp = []; + setsize(wp,fpSize); + setsize(text_wp,fpSize); + + # Retrieve route coordinates + for (var i=0; i<(fpSize); i += 1) + { + if (i == 0) { + var leg = fp.getWP(1); + append(coords,"N"~leg.path()[0].lat); + append(coords,"E"~leg.path()[0].lon); + append(cmds,2); + canvas.drawwp(group, leg.path()[0].lat,leg.path()[0].lon,fp.getWP(0).wp_name,i, wp); + i+=1; + } + var leg = fp.getWP(i); + append(coords,"N"~leg.path()[1].lat); + append(coords,"E"~leg.path()[1].lon); + append(cmds,4); + canvas.drawwp(group, leg.path()[1].lat,leg.path()[1].lon,leg.wp_name,i, wp); + } + + # Update route coordinates + debug.dump(cmds); + debug.dump(coords); + route.setDataGeo(cmds, coords); + updatewp(0); +} \ No newline at end of file diff --git a/Nasal/canvas/map/route.layer b/Nasal/canvas/map/route.layer new file mode 100644 index 000000000..5981abe82 --- /dev/null +++ b/Nasal/canvas/map/route.layer @@ -0,0 +1,10 @@ +var RouteLayer = {}; + +RouteLayer.new = func(group,name) { + var m=Layer.new(group, name, RouteModel); + m.setDraw (func draw_layer(layer:m, callback: draw_route, lod:0) ); + return m; +} + +register_layer("route", RouteLayer); + diff --git a/Nasal/canvas/map/route.model b/Nasal/canvas/map/route.model new file mode 100644 index 000000000..ae43efb07 --- /dev/null +++ b/Nasal/canvas/map/route.model @@ -0,0 +1,26 @@ + +var RouteModel = {route_monitor:nil}; +RouteModel.new = func make(LayerModel, RouteModel); + +RouteModel.init = func { + me._view.reset(); + if (!getprop("/autopilot/route-manager/active")) + print("Cannot draw route, route manager inactive!") and return; + + print("TODO: route.model is still an empty stub, see route.draw instead"); + + ## TODO: all the model stuff is still inside the draw file for now, this just ensures that it will be called once + foreach(var t; [nil] ) + me.push(t); + + me.notifyView(); + + #FIXME: segfault of the day: use this layer once without a route, and then with a route - and BOOM, need to investigate. + + # TODO: should register a route manager listener here to update itself whenever the route/active WPT changes! + # also, if the layer is used in a dialog, the listener should be removed when the dialog is closed + if (me.route_monitor == nil) # FIXME: remove this listener durint reinit + me.route_monitor=setlistener("/autopilot/route-manager/active", func me.init() ); # this can probably be shared (singleton), because all canvases will be displaying same route ??? +} + + diff --git a/Nasal/canvas/map/runway-nd.draw b/Nasal/canvas/map/runway-nd.draw new file mode 100644 index 000000000..e045bd705 --- /dev/null +++ b/Nasal/canvas/map/runway-nd.draw @@ -0,0 +1,53 @@ + +var draw_rwy_nd = func (group, rwy, controller=nil, lod=nil) { + # print("drawing runways-nd"); + canvas._draw_rwy_nd(group,rwy.lat,rwy.lon,rwy.length,rwy.width,rwy.heading); +} + + +## +# TODO: this is not yet a real draw callback ... (wrong signature, not yet integrated) + +var _draw_rwy_nd = func (group, lat, lon, length, width, rwyhdg) { + var apt = airportinfo("EHAM"); + var rwy = apt.runway("18R"); + + var crds = []; + var coord = geo.Coord.new(); + width=width*20; # Else rwy is too thin to be visible + coord.set_latlon(lat, lon); + coord.apply_course_distance(rwyhdg, -14.2*NM2M); + append(crds,"N"~coord.lat()); + append(crds,"E"~coord.lon()); + coord.apply_course_distance(rwyhdg, 28.4*NM2M+length); + append(crds,"N"~coord.lat()); + append(crds,"E"~coord.lon()); + icon_rwy = group.createChild("path", "rwy-cl") + .setStrokeLineWidth(3) + .setDataGeo([2,4],crds) + .setColor(1,1,1) + .setStrokeDashArray([10, 20, 10, 20, 10]); + var crds = []; + coord.set_latlon(lat, lon); + coord.apply_course_distance(rwyhdg + 90, width/2); + append(crds,"N"~coord.lat()); + append(crds,"E"~coord.lon()); + coord.apply_course_distance(rwyhdg, length); + append(crds,"N"~coord.lat()); + append(crds,"E"~coord.lon()); + icon_rwy = group.createChild("path", "rwy") + .setStrokeLineWidth(3) + .setDataGeo([2,4],crds) + .setColor(1,1,1); + var crds = []; + coord.apply_course_distance(rwyhdg - 90, width); + append(crds,"N"~coord.lat()); + append(crds,"E"~coord.lon()); + coord.apply_course_distance(rwyhdg, -length); + append(crds,"N"~coord.lat()); + append(crds,"E"~coord.lon()); + icon_rwy = group.createChild("path", "rwy") + .setStrokeLineWidth(3) + .setDataGeo([2,4],crds) + .setColor(1,1,1); +} diff --git a/Nasal/canvas/map/runway-nd.layer b/Nasal/canvas/map/runway-nd.layer new file mode 100644 index 000000000..e7c27cf0f --- /dev/null +++ b/Nasal/canvas/map/runway-nd.layer @@ -0,0 +1,8 @@ +var RunwayNDLayer = {}; + RunwayNDLayer.new = func(group, name) { + var m=Layer.new(group, name, RunwayNDModel ); + m.setDraw( func draw_layer(layer: m, callback: draw_rwy_nd, lod:0 ) ); + return m; +} +register_layer("runway-nd", RunwayNDLayer); + diff --git a/Nasal/canvas/map/runway-nd.model b/Nasal/canvas/map/runway-nd.model new file mode 100644 index 000000000..ee7db60da --- /dev/null +++ b/Nasal/canvas/map/runway-nd.model @@ -0,0 +1,26 @@ +var RunwayNDModel = {}; + RunwayNDModel.new = func make( LayerModel, RunwayNDModel ); + + RunwayNDModel.init = func { + me._view.reset(); + + # check if RM is active and bail out if not +if (!getprop("/autopilot/route-manager/active")) + print("runway-nd.model: Cannot access flight plan, route manager inactive!") and return; + + + + var desApt = airportinfo(getprop("/autopilot/route-manager/destination/airport")); + var depApt = airportinfo(getprop("/autopilot/route-manager/departure/airport")); + var desRwy = desApt.runway(getprop("/autopilot/route-manager/destination/runway")); + var depRwy = depApt.runway(getprop("/autopilot/route-manager/departure/runway")); + + + me.push(depRwy); + me.push(desRwy); + + + me.notifyView(); +} + + diff --git a/Nasal/canvas/map/runways.draw b/Nasal/canvas/map/runways.draw index 9c8f34d75..ab4b1d504 100644 --- a/Nasal/canvas/map/runways.draw +++ b/Nasal/canvas/map/runways.draw @@ -1,9 +1,9 @@ #TODO: split: draw_single_runway(pos) -var draw_runways = func(group, apt,lod) { + +var draw_runways = func(group, apt, controller=nil, lod=0) { DEBUG and print("Drawing runways for:", apt.id); -# var group = group.createChild("group", "apt-"~apt.id); -# group = group.createChild("group", "runways"); + foreach(var rw1; apt.runwaysWithoutReciprocals()) { var clr = SURFACECOLORS[rw1.surface]; diff --git a/Nasal/canvas/map/runways.layer b/Nasal/canvas/map/runways.layer index 10b990f72..2845acaad 100644 --- a/Nasal/canvas/map/runways.layer +++ b/Nasal/canvas/map/runways.layer @@ -1,10 +1,10 @@ #TODO: use custom Model/DataProvider var RunwayLayer = {}; # make(Layer); - RunwayLayer.new = func(group, name) { - # print("Setting up new TestLayer"); - var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! - m.setDraw( func draw_layer(layer: m, callback: draw_runways, lod:0 ) ); - return m; +RunwayLayer.new = func(group, name) { + # print("Setting up new TestLayer"); + var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! + m.setDraw( func draw_layer(layer: m, callback: draw_runways, lod:0 ) ); + return m; } register_layer("runways", RunwayLayer); diff --git a/Nasal/canvas/map/taxiways.draw b/Nasal/canvas/map/taxiways.draw index 6cfaf337f..522627d3f 100644 --- a/Nasal/canvas/map/taxiways.draw +++ b/Nasal/canvas/map/taxiways.draw @@ -1,41 +1,39 @@ var draw_taxiways = func(group, apt, lod) { # TODO: the LOD arg isn't stricly needed here, # the layer is a conventional canvas group, so it can access its map # parent and just read the "range" property to do LOD handling - group.set("z-index",-100) # HACK: we need to encapsulate this - .set("stroke", "none"); - # var group = group.createChild("group", "apt-"~apt.id); #FIXME: we don't need to use two nested groups for each taxiway - performance? - # group = group.createChild("group", "taxiways"); - # print("drawing taxiways for:", apt.id); - # Taxiways drawn first so the runways and parking positions end up on top. + group.set("z-index",-100) # HACK: we need to encapsulate this + .set("stroke", "none"); - # Preallocate all paths at once to gain some speed - var taxi_paths = group.createChildren("path", size(apt.taxiways)); - var i = 0; - foreach(var taxi; apt.taxiways) - { - var clr = SURFACECOLORS[taxi.surface]; - if (clr == nil) { clr = SURFACECOLORS[0]}; + # print("drawing taxiways for:", apt.id); + # Taxiways drawn first so the runways and parking positions end up on top. - var txi = Runway.new(taxi); - var beg1 = txi.pointOffCenterline(0, 0.5 * taxi.width); - var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width); - var end1 = txi.pointOffCenterline(taxi.length, 0.5 * taxi.width); - var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width); + # Preallocate all paths at once to gain some speed + var taxi_paths = group.createChildren("path", size(apt.taxiways)); + var i = 0; + foreach(var taxi; apt.taxiways) { + var clr = SURFACECOLORS[taxi.surface]; + if (clr == nil) { clr = SURFACECOLORS[0]}; - taxi_paths[i].setColorFill(clr.r, clr.g, clr.b) - .setDataGeo - ( - [ canvas.Path.VG_MOVE_TO, - canvas.Path.VG_LINE_TO, - canvas.Path.VG_LINE_TO, - canvas.Path.VG_LINE_TO, - canvas.Path.VG_CLOSE_PATH ], - [ beg1[0], beg1[1], - beg2[0], beg2[1], - end2[0], end2[1], - end1[0], end1[1] ] - ); - i += 1; - } + var txi = Runway.new(taxi); + var beg1 = txi.pointOffCenterline(0, 0.5 * taxi.width); + var beg2 = txi.pointOffCenterline(0, -0.5 * taxi.width); + var end1 = txi.pointOffCenterline(taxi.length, 0.5 * taxi.width); + var end2 = txi.pointOffCenterline(taxi.length, -0.5 * taxi.width); + + taxi_paths[i].setColorFill(clr.r, clr.g, clr.b) + .setDataGeo + ( + [ canvas.Path.VG_MOVE_TO, + canvas.Path.VG_LINE_TO, + canvas.Path.VG_LINE_TO, + canvas.Path.VG_LINE_TO, + canvas.Path.VG_CLOSE_PATH ], + [ beg1[0], beg1[1], + beg2[0], beg2[1], + end2[0], end2[1], + end1[0], end1[1] ] + ); + i += 1; + } } diff --git a/Nasal/canvas/map/taxiways.layer b/Nasal/canvas/map/taxiways.layer index 40b853cbd..4eb06d2d9 100644 --- a/Nasal/canvas/map/taxiways.layer +++ b/Nasal/canvas/map/taxiways.layer @@ -1,10 +1,10 @@ #TODO: use custom Model/DataProvider var TaxiwayLayer = {}; # make(Layer); - TaxiwayLayer.new = func(group, name) { - # print("Setting up new TestLayer"); - var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! - m.setDraw( func draw_layer(layer: m, callback: draw_taxiways, lod:0 ) ); - return m; +TaxiwayLayer.new = func(group, name) { + # print("Setting up new TestLayer"); + var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! + m.setDraw( func draw_layer(layer: m, callback: draw_taxiways, lod:0 ) ); + return m; } register_layer("taxiways", TaxiwayLayer); diff --git a/Nasal/canvas/map/tcas_arrow_a500.draw b/Nasal/canvas/map/tcas_arrow_a500.draw new file mode 100644 index 000000000..23b05a0bf --- /dev/null +++ b/Nasal/canvas/map/tcas_arrow_a500.draw @@ -0,0 +1,12 @@ +var draw_tcas_arrow_above_500 = func(group, lod=0) { + group.createChild("path") + .moveTo(0,-17) + .vertTo(17) + .lineTo(-10,0) + .moveTo(0,17) + .lineTo(10,0) + .setColor(1,1,1) + .setTranslation(25,0) + .setStrokeLineWidth(3); + +} diff --git a/Nasal/canvas/map/tcas_arrow_b500.draw b/Nasal/canvas/map/tcas_arrow_b500.draw new file mode 100644 index 000000000..0d07dea65 --- /dev/null +++ b/Nasal/canvas/map/tcas_arrow_b500.draw @@ -0,0 +1,14 @@ +var draw_tcas_arrow_below_500 = func(group) { + +group.createChild("path") + .moveTo(0,17) + .vertTo(-17) + .lineTo(-10,0) + .moveTo(0,-17) + .lineTo(10,0) + .setColor(1,1,1) + .setTranslation(25,0) + .setStrokeLineWidth(3); + + +} \ No newline at end of file diff --git a/Nasal/canvas/map/test.layer b/Nasal/canvas/map/test.layer index 069d1ee82..d2bd296b8 100644 --- a/Nasal/canvas/map/test.layer +++ b/Nasal/canvas/map/test.layer @@ -1,11 +1,11 @@ #TODO: use custom Model/DataProvider var TestLayer = {}; # make(Layer); - TestLayer.new = func(group, name) { - # print("Setting up new TestLayer"); - var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! - m.setDraw( func draw_layer(layer: m, callback: MAP_LAYERS["runways"], lod:0 ) ); - return m; +TestLayer.new = func(group, name) { + # print("Setting up new TestLayer"); + var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! + m.setDraw( func draw_layer(layer: m, callback: MAP_LAYERS["runways"], lod:0 ) ); + return m; } register_layer("airport_test", TestLayer); diff --git a/Nasal/canvas/map/tower.draw b/Nasal/canvas/map/tower.draw index eb4995e99..0cac8f54e 100644 --- a/Nasal/canvas/map/tower.draw +++ b/Nasal/canvas/map/tower.draw @@ -1,19 +1,19 @@ var draw_tower = func (group, apt,lod) { - var group = group.createChild("group", "tower"); - # TODO: move to map_elements.nas (tower, runway, parking etc) - # i.e.: set_element(group, "tower", "style"); - var icon_tower = - group.createChild("path", "tower") - .setStrokeLineWidth(1) - .setScale(1.5) - .setColor(0.2,0.2,1.0) - .moveTo(-3, 0) - .vert(-10) - .line(-3, -10) - .horiz(12) - .line(-3, 10) - .vert(10); - - icon_tower.setGeoPosition(apt.tower().lat, apt.tower().lon); + var group = group.createChild("group", "tower"); + # TODO: move to map_elements.nas (tower, runway, parking etc) + # i.e.: set_element(group, "tower", "style"); + var icon_tower = + group.createChild("path", "tower") + .setStrokeLineWidth(1) + .setScale(1.5) + .setColor(0.2,0.2,1.0) + .moveTo(-3, 0) + .vert(-10) + .line(-3, -10) + .horiz(12) + .line(-3, 10) + .vert(10); + + icon_tower.setGeoPosition(apt.tower().lat, apt.tower().lon); } diff --git a/Nasal/canvas/map/tower.layer b/Nasal/canvas/map/tower.layer index 10704d0ca..099b2af78 100644 --- a/Nasal/canvas/map/tower.layer +++ b/Nasal/canvas/map/tower.layer @@ -1,8 +1,9 @@ var TowerLayer = {}; - TowerLayer.new = func(group, name) { - var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! - m.setDraw( func draw_layer(layer: m, callback: draw_tower, lod:0 ) ); - return m; +TowerLayer.new = func(group, name) { + var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! + m.setDraw( func draw_layer(layer: m, callback: draw_tower, lod:0 ) ); + return m; } + register_layer("towers", TowerLayer); diff --git a/Nasal/canvas/map/traffic.draw b/Nasal/canvas/map/traffic.draw new file mode 100644 index 000000000..12602a41b --- /dev/null +++ b/Nasal/canvas/map/traffic.draw @@ -0,0 +1,83 @@ +var draw_traffic = func(group, traffic, lod=0) +{ + var a = traffic; + var tcas_group = group; + + var callsign = a.getNode("callsign").getValue(); + # print("Drawing traffic for:", callsign ); + var lat = a.getNode("position/latitude-deg").getValue(); + var lon = a.getNode("position/longitude-deg").getValue(); + var alt = a.getNode("position/altitude-ft").getValue(); + var dist = a.getNode("radar/range-nm").getValue(); + var threatLvl = a.getNode("tcas/threat-level",1).getValue(); + var raSense = a.getNode("tcas/ra-sense",1).getValue(); + var vspeed = a.getNode("velocities/vertical-speed-fps").getValue()*60; + var altDiff = alt - getprop("/position/altitude-ft"); + + var tcas_grp = tcas_group.createChild("group", callsign); + + var text_tcas = tcas_grp.createChild("text") + .setDrawMode( canvas.Text.TEXT ) + .setText(sprintf("%+02.0f",altDiff/100)) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") + .setColor(1,1,1) + .setFontSize(28) + .setAlignment("center-center"); + if (altDiff > 0) + text_tcas.setTranslation(0,-40); + else + text_tcas.setTranslation(0,40); + if(vspeed >= 500) { + var arrow_tcas = canvas.draw_tcas_arrow_above_500(tcas_grp); + } elsif (vspeed < 500) { + var arrow_tcas = canvas.draw_tcas_arrow_below_500(tcas_grp); + } + + ## TODO: threat level symbols should also be moved to *.draw files + var icon_tcas = tcas_grp.createChild("path") + .setStrokeLineWidth(3); + if (threatLvl == 3) { + # resolution advisory + icon_tcas.moveTo(-17,-17) + .horiz(34) + .vert(34) + .horiz(-34) + .close() + .setColor(1,0,0) + .setColorFill(1,0,0); + text_tcas.setColor(1,0,0); + arrow_tcas.setColor(1,0,0); + } elsif (threatLvl == 2) { + # traffic advisory + icon_tcas.moveTo(-17,0) + .arcSmallCW(17,17,0,34,0) + .arcSmallCW(17,17,0,-34,0) + .setColor(1,0.5,0) + .setColorFill(1,0.5,0); + text_tcas.setColor(1,0.5,0); + arrow_tcas.setColor(1,0.5,0); + } elsif (threatLvl == 1) { + # proximate traffic + icon_tcas.moveTo(-10,0) + .lineTo(0,-17) + .lineTo(10,0) + .lineTo(0,17) + .close() + .setColor(1,1,1) + .setColorFill(1,1,1); + } else { + # other traffic + icon_tcas.moveTo(-10,0) + .lineTo(0,-17) + .lineTo(10,0) + .lineTo(0,17) + .close() + .setColor(1,1,1); + } + + tcas_grp.setGeoPosition(lat, lon) + .set("z-index",1); + + + #settimer(func drawtraffic(group), 2); # updates are handled by the model, not by the view! +}; diff --git a/Nasal/canvas/map/traffic.layer b/Nasal/canvas/map/traffic.layer new file mode 100644 index 000000000..121d1e1cc --- /dev/null +++ b/Nasal/canvas/map/traffic.layer @@ -0,0 +1,11 @@ +var MPTrafficLayer = {}; +MPTrafficLayer.new = func(group,name, controller=nil) { + var m=Layer.new(group, name, MPTrafficModel); + m._model._controller = controller; + m.setDraw (func draw_layer(layer:m, callback: draw_traffic, lod:0) ); + return m; +} + +register_layer("mp-traffic", MPTrafficLayer); +# TODO: also register AI traffic layer here + diff --git a/Nasal/canvas/map/traffic.model b/Nasal/canvas/map/traffic.model new file mode 100644 index 000000000..5665da34b --- /dev/null +++ b/Nasal/canvas/map/traffic.model @@ -0,0 +1,44 @@ +var MPTrafficModel = {}; +MPTrafficModel.new = func make(LayerModel, MPTrafficModel); + +MPTrafficModel.init = func { + var pos = geo.Coord.new(); # FIXME: all of these should be instance variables + var myPosition = geo.Coord.new(); + var myPositionVec = me._controller['get_position'](); + myPosition.set_latlon( myPositionVec[0], myPositionVec[1]); + var max_dist_nm = me._controller['query_range'](); + + ## + # uncomment this for showing MP traffic + # var traffic_type = "multiplayer"; + # and use this for development purposes: + var traffic_type = "aircraft"; + + #if (traffic_type == "aircraft") + # print("INFO: traffic.model is still showing AI traffic instead of MP traffic!"); + + me._view.reset(); # hides: removeAllChildren() + var traffic = props.globals.initNode("/ai/models/").getChildren( traffic_type ); + #print("Total traffic:", size(traffic)); + foreach(var t; traffic) { + pos.set_latlon( t.getNode("position/latitude-deg").getValue(), + t.getNode("position/longitude-deg").getValue() + ); + + if (pos.distance_to( myPosition ) <= max_dist_nm*NM2M ) { + #print("Pushing: ", t.getNode("callsign").getValue() ); + me.push(t); + } + } + #print("traffic.model: Query range:", max_dist_nm, " Items:", me.hasData() ); + + + + me.notifyView(); + + # update itself FIXME: this needs to be killed by the controller (e.g. when tcas layer is disabled) + # and the interval needs to be configurable via the controller + # so better use maketimer() here + settimer(func me.init(), 2); +} + diff --git a/Nasal/canvas/map/vor.draw b/Nasal/canvas/map/vor.draw new file mode 100644 index 000000000..edde296f2 --- /dev/null +++ b/Nasal/canvas/map/vor.draw @@ -0,0 +1,59 @@ +var draw_vor = func (group, vor, controller=nil, lod = 0) { + + if (0) { + if (controller == nil) + print("Ooops, VOR controller undefined!"); + else + debug.dump( controller ); + } + + var lat = vor.lat; + var lon = vor.lon; + var name = vor.id; + var freq = vor.frequency; + var range = vor.range_nm; + + # FIXME: Hack - implement a real controller for this! + var rangeNm = (controller!=nil) ? controller['query_range']() : 50; + + var vor_grp = group.createChild("group",name); + var icon_vor = vor_grp.createChild("path", "vor-icon-" ~ name) + .moveTo(-15,0) + .lineTo(-7.5,12.5) + .lineTo(7.5,12.5) + .lineTo(15,0) + .lineTo(7.5,-12.5) + .lineTo(-7.5,-12.5) + .close() + .setStrokeLineWidth(3) + .setColor(0,0.6,0.85); + + # next check if the current VOR is tuned, if so show it + # for this to work, we need a controller hash with an "is_tuned" member that points to a callback + # (set up by the layer managing this view) + # for an example, see the NavDisplay.newMFD() in navdisplay.mfd + + if (controller != nil and controller['is_tuned'](freq/100)) { + # print("VOR is tuned:", name); + var radius = (range/rangeNm)*345; + var range_vor = vor_grp.createChild("path", "range-vor-" ~ name) + .moveTo(-radius,0) + .arcSmallCW(radius,radius,0,2*radius,0) + .arcSmallCW(radius,radius,0,-2*radius,0) + .setStrokeLineWidth(3) + .setStrokeDashArray([5, 15, 5, 15, 5]) + .setColor(0,1,0); + + var course = controller['get_tuned_course'](freq/100); + vor_grp.createChild("path", "radial-vor-" ~ name) + .moveTo(0,-radius) + .vert(2*radius) + .setStrokeLineWidth(3) + .setStrokeDashArray([15, 5, 15, 5, 15]) + .setColor(0,1,0) + .setRotation(course*D2R); + icon_vor.setColor(0,1,0); + } + vor_grp.setGeoPosition(lat, lon) + .set("z-index",3); +} diff --git a/Nasal/canvas/map/vor.layer b/Nasal/canvas/map/vor.layer new file mode 100644 index 000000000..c7ebb32f6 --- /dev/null +++ b/Nasal/canvas/map/vor.layer @@ -0,0 +1,10 @@ +var VORLayer = {}; +VORLayer.new = func(group,name, controller) { + var m=Layer.new(group, name, VORModel ); + m._controller = controller; + m.setDraw (func draw_layer(layer:m, callback: draw_vor, lod:0) ); + return m; +} + +register_layer("vor", VORLayer); + diff --git a/Nasal/canvas/map/vor.model b/Nasal/canvas/map/vor.model new file mode 100644 index 000000000..37c0db71f --- /dev/null +++ b/Nasal/canvas/map/vor.model @@ -0,0 +1,15 @@ +var VORModel = {}; +VORModel.new = func make( LayerModel, VORModel ); + +VORModel.init = func { + #debug.dump( me._controller ); + me._view.reset(); + + var results = positioned.findWithinRange( me._controller.query_range()*2 ,"vor"); + foreach(result; results) { + me.push(result); + } + + me.notifyView(); +} + diff --git a/Nasal/canvas/map/waypoint.draw b/Nasal/canvas/map/waypoint.draw new file mode 100644 index 000000000..18be45a46 --- /dev/null +++ b/Nasal/canvas/map/waypoint.draw @@ -0,0 +1,34 @@ +## +# Draw a waypoint symbol and waypoint name (Gijs' 744 ND.nas code) + +# +var drawwp = func (group, lat, lon, name, i, wp) { + var wp_group = group.createChild("group","wp"); + wp[i] = wp_group.createChild("path", "wp-" ~ i) + .setStrokeLineWidth(3) + .moveTo(0,-25) + .lineTo(-5,-5) + .lineTo(-25,0) + .lineTo(-5,5) + .lineTo(0,25) + .lineTo(5,5) + .lineTo(25,0) + .lineTo(5,-5) + .setColor(1,1,1) + .close(); + ##### + # The commented code leads to a segfault when a route is replaced by a new one + ##### + # + # text_wp[i] = wp_group.createChild("text", "wp-text-" ~ i) + # + var text_wps = wp_group.createChild("text", "wp-text-" ~ i) + .setDrawMode( canvas.Text.TEXT ) + .setText(name) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") + .setFontSize(28) + .setTranslation(25,35) + .setColor(1,0,1); + wp_group.setGeoPosition(lat, lon) + .set("z-index",4); +}; diff --git a/Nasal/canvas/svg.nas b/Nasal/canvas/svg.nas index 181b0c5d8..490a94d8b 100644 --- a/Nasal/canvas/svg.nas +++ b/Nasal/canvas/svg.nas @@ -444,7 +444,7 @@ var parsesvg = func(group, path, options = nil) var el_src = id_dict[ substr(ref, 1) ]; if( el_src == nil ) - return print("parsesvg: Reference to unknown element (" ~ ref ~ ")"); + return printlog("info", "parsesvg: Reference to unknown element (" ~ ref ~ ")"); # Create new element and copy sub branch from source node pushElement(el_src._node.getName(), attr['id']); @@ -455,7 +455,7 @@ var parsesvg = func(group, path, options = nil) } else { - print("parsesvg: skipping unknown element '" ~ name ~ "'"); + printlog("info", "parsesvg: skipping unknown element '" ~ name ~ "'"); skip = level; return; } diff --git a/Nasal/geo.nas b/Nasal/geo.nas index bfbc00c1a..7408b1e0f 100644 --- a/Nasal/geo.nas +++ b/Nasal/geo.nas @@ -324,4 +324,64 @@ var viewer_position = func { return Coord.new().set_xyz(x, y, z); } +# A object to handle differential positioned searches: +# searchCmd executes and returns the actual search, +# onAdded and onRemoved are callbacks, +# and obj is a "me" reference (defaults to "me" in the +# caller's namespace). +var PositionedSearch = { + new: func(searchCmd, onAdded, onRemoved, obj=nil) { + return { + parents:[PositionedSearch], + obj: obj == nil ? caller(1)[0]["me"] : obj, + searchCmd: searchCmd, + onAdded: onAdded, + onRemoved: onRemoved, + result: [], + }; + }, + _equals: func(a,b) { + if (a == nil or b == nil) return 0; + return (a == b or a.id == b.id); + }, + condense: func(vec) { + var ret = []; + foreach (var e; vec) + if (e != nil) append(ret, e); + return ret; + }, + diff: func(old, new) { + var removed = old~[]; #copyvec + var added = new~[]; + # Mark common elements from removed and added: + forindex (OUTER; var i; removed) + forindex (var j; new) + if (me._equals(removed[i], added[j])) { + removed[i] = added[j] = nil; + continue OUTER; + } + # And remove those common elements, returning the result: + return [new, me.condense(removed), me.condense(added)]; + }, + update: func(searchCmd=nil) { + if (searchCmd == nil) searchCmd = me.searchCmd; + (me.result, var removed, var added) = me.diff(me.result, call(searchCmd, nil, me.obj)); + foreach (var e; removed) + call(me.onRemoved, [e], me.obj); + foreach (var e; added) + call(me.onAdded, [e], me.obj); + }, + # this is the worst case scenario: switching from 640 to 320 (or vice versa) + test: func(from=640, to=320) { + var s= geo.PositionedSearch.new( + func positioned.findWithinRange(from, 'fix'), + func print('added:', arg[0].id), + func print('removed:', arg[0].id) + ); + debug.benchmark('Toggle '~from~'nm/'~to~'nm', func { + s.update(); + s.update( func positioned.findWithinRange(to, 'fix') ); + }); # ~ takes + }, # of test +};