From 37c005c222042bc1e420054db1bbabebdffae031 Mon Sep 17 00:00:00 2001 From: Philosopher Date: Mon, 28 Apr 2014 21:26:32 -0500 Subject: [PATCH] Many MapStructure/NavDisplay updates See the clone at https://gitorious.org/fg/canvas-hackers-fgdata/source/topics/canvas-radar: --- Nasal/canvas/MapStructure.nas | 456 +++++++++++++++++------- Nasal/canvas/api.nas | 85 ++++- Nasal/canvas/map/APS.lcontroller | 27 ++ Nasal/canvas/map/APS.scontroller | 9 + Nasal/canvas/map/APS.symbol | 16 + Nasal/canvas/map/APT.lcontroller | 35 ++ Nasal/canvas/map/APT.scontroller | 13 + Nasal/canvas/map/APT.symbol | 52 +++ Nasal/canvas/map/DME.lcontroller | 35 +- Nasal/canvas/map/DME.scontroller | 7 +- Nasal/canvas/map/DME.symbol | 138 ++++++- Nasal/canvas/map/FIX.lcontroller | 21 +- Nasal/canvas/map/FIX.scontroller | 5 +- Nasal/canvas/map/FIX.symbol | 41 ++- Nasal/canvas/map/NDB.lcontroller | 9 +- Nasal/canvas/map/NDB.scontroller | 4 +- Nasal/canvas/map/NDB.symbol | 1 + Nasal/canvas/map/TFC.lcontroller | 83 ++--- Nasal/canvas/map/TFC.scontroller | 14 +- Nasal/canvas/map/TFC.symbol | 17 +- Nasal/canvas/map/VOR.lcontroller | 27 +- Nasal/canvas/map/VOR.scontroller | 5 +- Nasal/canvas/map/VOR.symbol | 118 ++++-- Nasal/canvas/map/WPT.lcontroller | 8 +- Nasal/canvas/map/WPT.scontroller | 1 + Nasal/canvas/map/WPT.symbol | 1 + Nasal/canvas/map/aircraftpos.controller | 81 ++++- Nasal/canvas/map/airplane-symbol.draw | 1 + Nasal/canvas/map/airplane-symbol.layer | 1 + Nasal/canvas/map/airplane-symbol.model | 1 + Nasal/canvas/map/airports-nd.draw | 1 + Nasal/canvas/map/airports-nd.layer | 1 + Nasal/canvas/map/airports-nd.model | 1 + Nasal/canvas/map/airports.model | 1 + Nasal/canvas/map/altitude-profile.draw | 1 + Nasal/canvas/map/dme.draw | 1 + Nasal/canvas/map/dme.layer | 1 + Nasal/canvas/map/dme.model | 1 + Nasal/canvas/map/fix.draw | 1 + Nasal/canvas/map/fixes.layer | 1 + Nasal/canvas/map/fixes.model | 1 + Nasal/canvas/map/navaid.draw | 1 + Nasal/canvas/map/navaids.layer | 1 + Nasal/canvas/map/navaids.model | 1 + Nasal/canvas/map/navdisplay.mfd | 216 +++++------ Nasal/canvas/map/parking.draw | 1 + Nasal/canvas/map/parking.layer | 1 + Nasal/canvas/map/route.draw | 1 + Nasal/canvas/map/route.layer | 1 + Nasal/canvas/map/route.model | 1 + Nasal/canvas/map/runway-nd.draw | 1 + Nasal/canvas/map/runway-nd.layer | 1 + Nasal/canvas/map/runway-nd.model | 1 + Nasal/canvas/map/runways.draw | 1 + Nasal/canvas/map/runways.layer | 1 + Nasal/canvas/map/storm.draw | 1 + Nasal/canvas/map/storms.layer | 1 + Nasal/canvas/map/storms.model | 1 + Nasal/canvas/map/taxiways.draw | 1 + Nasal/canvas/map/taxiways.layer | 1 + Nasal/canvas/map/tcas_arrow_a500.draw | 1 + Nasal/canvas/map/tcas_arrow_b500.draw | 1 + Nasal/canvas/map/test.layer | 1 + Nasal/canvas/map/tower.draw | 1 + Nasal/canvas/map/tower.layer | 1 + Nasal/canvas/map/traffic.draw | 1 + Nasal/canvas/map/traffic.layer | 1 + Nasal/canvas/map/traffic.model | 1 + Nasal/canvas/map/vor.draw | 1 + Nasal/canvas/map/vor.layer | 1 + Nasal/canvas/map/vor.model | 1 + Nasal/canvas/map/waypoint.draw | 1 + Nasal/geo.nas | 9 +- 73 files changed, 1180 insertions(+), 398 deletions(-) create mode 100644 Nasal/canvas/map/APS.lcontroller create mode 100644 Nasal/canvas/map/APS.scontroller create mode 100644 Nasal/canvas/map/APS.symbol create mode 100644 Nasal/canvas/map/APT.lcontroller create mode 100644 Nasal/canvas/map/APT.scontroller create mode 100644 Nasal/canvas/map/APT.symbol diff --git a/Nasal/canvas/MapStructure.nas b/Nasal/canvas/MapStructure.nas index 0afdcd213..1bc20fe01 100644 --- a/Nasal/canvas/MapStructure.nas +++ b/Nasal/canvas/MapStructure.nas @@ -1,4 +1,9 @@ +################################################################################ +## MapStructure mapping/charting framework for Nasal/Canvas, by Philosopher +## See: http://wiki.flightgear.org/MapStructure +############################################################################### var _MP_dbg_lvl = "info"; +#var _MP_dbg_lvl = "alert"; var dump_obj = func(m) { var h = {}; @@ -9,32 +14,41 @@ var dump_obj = func(m) { }; ## -# must be either of: -# 1) draw* callback, 2) SVG filename, 3) Drawable class (with styling/LOD support) -var SymbolDrawable = { - new: func() { - }, +# for LOD handling (i.e. airports/taxiways/runways) +var RangeAware = { + new: func { + return {parents:[RangeAware] }; + }, + del: func, + notifyRangeChange: func die("RangeAware.notifyRangeChange() must be provided by sub-class"), }; -## wrapper for each element -## i.e. keeps the canvas and texture map coordinates +## wrapper for each cached element +## i.e. keeps the canvas and texture map coordinates for the corresponding raster image var CachedElement = { - new: func(canvas_path, name, source, offset) { + new: func(canvas_path, name, source, size, offset) { var m = {parents:[CachedElement] }; + if (isa(canvas_path, canvas.Canvas)) { + canvas_path = canvas_path.getPath(); + } m.canvas_src = canvas_path; m.name = name; m.source = source; + m.size = size; m.offset = offset; return m; }, # new() - render: func(group) { + + render: func(group, trans0=0, trans1=0) { # create a raster image child in the render target/group - return - group.createChild("image", me.name) + var n = group.createChild("image", me.name) .setFile( me.canvas_src ) - # TODO: fix .setSourceRect() to accept a single vector for coordinates ... - .setSourceRect(left:me.source[0],top:me.source[1],right:me.source[2],bottom:me.source[3] , normalized:0) - .setTranslation(me.offset); # FIXME: make sure this stays like this and isn't overridden + # TODO: fix .setSourceRect() to accept a single vector for texture map coordinates ... + .setSourceRect(left:me.source[0],top:me.source[1],right:me.source[2],bottom:me.source[3], normalized:0) + .setSize(me.size) + .setTranslation(trans0,trans1); + n.createTransform().setTranslation(me.offset); + return n; }, # render() }; # of CachedElement @@ -77,10 +91,13 @@ var SymbolCache = { "view": m.canvas_sz, "mipmapping": 1 }); - + m.canvas_texture.setColorBackground(0, 0, 0, 0); #rgba # add a placement m.canvas_texture.addPlacement( {"type": "ref"} ); + m.path = m.canvas_texture.getPath(); + m.root = m.canvas_texture.createGroup("entries"); + return m; }, add: func(name, callback, draw_mode=0) { @@ -90,7 +107,7 @@ var SymbolCache = { # get canvas texture that we use as cache # get next free spot in texture (column/row) # run the draw callback and render into a group - var gr = me.canvas_texture.createGroup(); + var gr = me.root.createChild("group",name); gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0, me.next_free[1] + me.image_sz[1]*draw_mode1); #settimer(func debug.dump ( gr.getTransformedBounds() ), 0); # XXX: these are only updated when rendered @@ -102,17 +119,27 @@ var SymbolCache = { # get the bounding box, i.e. coordinates for texture map, or use the .setTranslation() params var coords = me.next_free~me.next_free; foreach (var i; [0,1]) - coords[i+2] += me.image_sz[i]; + coords[i+1] += me.image_sz[i]; + foreach (var i; [0,1]) + coords[i*2+1] = me.canvas_sz[i] - coords[i*2+1]; # get the offset we used to position correctly in the bounds of the canvas - var offset = [me.image_sz[0]*draw_mode0, me.image_sz[1]*draw_mode1]; - # store texture map coordinates in lookup map using the name as identifier - me.dict[name] = CachedElement.new(me.canvas_texture.getPath(), name, coords, offset ); + var offset = [-me.image_sz[0]*draw_mode0, -me.image_sz[1]*draw_mode1]; + # update next free position in cache (column/row) me.next_free[0] += me.image_sz[0]; if (me.next_free[0] >= me.canvas_sz[0]) { me.next_free[0] = 0; me.next_free[1] += me.image_sz[1] } if (me.next_free[1] >= me.canvas_sz[1]) die("SymbolCache: ran out of space after adding '"~name~"'"); + + # store texture map coordinates in lookup map using the name as identifier + return me.dict[name] = CachedElement.new( + canvas_path: me.path, + name: name, + source: coords, + size:me.image_sz, + offset: offset, + ); }, # add() get: func(name) { if(!contains(me.dict,name)) die("No SymbolCache entry for key:"~ name); @@ -131,8 +158,9 @@ var Symbol = { 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); + # @param layer The #SymbolLayer this is a child of. + new: func(type, group, layer, arg...) { + var ret = call((var class = me.get(type)).new, [group, layer]~arg, class); ret.element.set("symbol-type", type); return ret; }, @@ -147,6 +175,7 @@ var Symbol = { die("del() not implemented for this symbol type!"), }; # of Symbol + Symbol.Controller = { # Static/singleton: registry: {}, @@ -176,43 +205,80 @@ 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' or ghosttype(obj)=='Fix' or ghosttype(obj)=='flightplan-leg') - return getpos_fromghost(obj); - else - die("bad ghost of type '"~ghosttype(obj)~"'"); - if (typeof(obj) == 'hash') - if (isa(obj, geo.Coord)) - return subvec(obj.latlon(), 0, 2); - if (isa(obj, props.Node)) - return [ - obj.getValue("position/latitude-deg") or obj.getValue("latitude-deg"), - obj.getValue("position/longitude-deg") or obj.getValue("longitude-deg") - ]; - if (contains(obj,'lat') and contains(obj,'lon')) - return [obj.lat, obj.lon]; - - debug.dump(obj); - die("no suitable getpos() found! Of type: "~typeof(obj)); +# to add support for additional ghosts, just append them to the vector below, possibly at runtime: +var supported_ghosts = ['positioned','Navaid','Fix','flightplan-leg','FGAirport']; +var is_positioned_ghost = func(obj) { + var gt = ghosttype(obj); + foreach(var ghost; supported_ghosts) { + if (gt == ghost) return 1; # supported ghost was found + } + return 0; # not a known/supported ghost }; -Symbol.Controller.equals = func(l, r) { - if (l == r) return 1; - var t = typeof(l); - if (t == 'ghost') - return 0;#l.id == r.id; - if (t == 'hash') - if (isa(l, props.Node)) - return l.equals(r); - else { - foreach (var k; keys(l)) - if (l[k] != r[k]) return 0; - return 1; +# Generic getpos: get lat/lon from any object: +# (geo.Coord and positioned ghost currently) +Symbol.Controller.getpos = func(obj, p=nil) { + if (obj == nil) die("Symbol.Controller.getpos received nil"); + if (p == nil) { + var ret = Symbol.Controller.getpos(obj, obj); + if (ret != nil) return ret; + if (contains(obj, "parents")) { + foreach (var p; obj.parents) { + var ret = Symbol.Controller.getpos(obj, p); + if (ret != nil) return ret; + } } - die("bad types"); + debug.dump(obj); + die("no suitable getpos() found! Of type: "~typeof(obj)); + } else { + if (typeof(p) == 'ghost') + if ( is_positioned_ghost(p) ) + return getpos_fromghost(obj); + else + die("bad/unsupported ghost of type '"~ghosttype(obj)~"' (see MapStructure.nas Symbol.Controller.getpos() to add new ghosts)"); + if (typeof(p) == 'hash') + if (p == geo.Coord) + return subvec(obj.latlon(), 0, 2); + if (p == props.Node) + return [ + obj.getValue("position/latitude-deg") or obj.getValue("latitude-deg"), + obj.getValue("position/longitude-deg") or obj.getValue("longitude-deg") + ]; + if (contains(p,'lat') and contains(p,'lon')) + return [obj.lat, obj.lon]; + return nil; + } +}; + +Symbol.Controller.equals = func(l, r, p=nil) { + if (l == r) return 1; + if (p == nil) { + var ret = Symbol.Controller.equals(l, r, l); + if (ret != nil) return ret; + if (contains(l, "parents")) { + foreach (var p; l.parents) { + var ret = Symbol.Controller.equals(l, r, p); + if (ret != nil) return ret; + } + } + debug.dump(obj); + die("no suitable equals() found! Of type: "~typeof(obj)); + } else { + if (typeof(p) == 'ghost') + if ( is_positioned_ghost(p) ) + return l.id == r.id; + else + die("bad/unsupported ghost of type '"~ghosttype(l)~"' (see MapStructure.nas Symbol.Controller.getpos() to add new ghosts)"); + if (typeof(p) == 'hash') + # Somewhat arbitrary convention: + # * l.equals(r) -- instance method, i.e. uses "me" and "arg[0]" + # * parent._equals(l,r) -- class method, i.e. uses "arg[0]" and "arg[1]" + if (contains(p, "equals")) + return l.equals(r); + if (contains(p, "_equals")) + return p._equals(l,r); + } + return nil; }; @@ -225,32 +291,28 @@ var assert_ms = func(hash, members...) var DotSym = { - parents: [Symbol], + parents: [Symbol], # TODO: use StyleableSymbol here to support styling and caching element_id: nil, # Static/singleton: makeinstance: func(name, hash) { if (!isa(hash, DotSym)) die("OOP error"); - #assert_ms(hash, - # "element_type", # type of Canvas element - # #"element_id", # optional Canvas id - # #"init", # initialize routine - # "draw", # init/update routine - # #getpos", # get position from model in [x_units,y_units] (optional) - #); return Symbol.add(name, hash); }, # For the instances returned from makeinstance: - # @param group The Canvas group to add this to. + # @param group The #Canvas.Group to add this to. + # @param layer The #SymbolLayer this is a child of. # @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) { + new: func(group, layer, model, controller=nil) { + if (me == nil) die(); var m = { parents: [me], group: group, + layer: layer, model: model, controller: controller == nil ? me.df_controller : controller, element: group.createChild( @@ -274,7 +336,7 @@ var DotSym = { 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 + if (size(err) and err[0] != "No such member: del") # ... and either catch or rethrow die(err[0]); me.element.del(); }, @@ -297,6 +359,7 @@ var DotSym = { if (size(pos) == 3) var (lat,lon,rotation) = pos; else die("bad position: "~debug.dump(pos)); + # print(me.model.id, ": Position lat/lon: ", lat, "/", lon); me.element.setGeoPosition(lat,lon); if (rotation != nil) me.element.setRotation(rotation); @@ -313,20 +376,36 @@ var SymbolLayer = { if ((var class = me.registry[type]) == nil) die("unknown type '"~type~"'"); else return class, -# Non-static: +# Default implementations/values: df_controller: nil, # default controller df_priority: nil, # default priority for display sorting + df_style: nil, 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 map The #Canvas.Map this is a member of. # @param controller A controller object (parents=[SymbolLayer.Controller]) # or implementation (parents[0].parents=[SymbolLayer.Controller]). - new: func(group, controller=nil) { + new: func(group, map, controller=nil, style=nil, options=nil) { + #print("Creating new Layer instance"); + if (me == nil) die(); 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) + map: map, + group: group.createChild("group", me.type), # TODO: the id is not properly set, but would be useful for debugging purposes (VOR, FIXES, NDB etc) list: [], + options: options, }; + m.setVisible(); + + # print("Layer setup options:", m.options!=nil); + # do no overwrite the default style if style is nil: + if (style != nil and typeof(style)=='hash') { + #print("Setting up a custom style!"); + m.style = style; + } else m.style = me.df_style; + + # debug.dump(m.style); m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m); # FIXME: hack to expose type of layer: if (caller(1)[1] == Map.addLayer) { @@ -349,10 +428,39 @@ var SymbolLayer = { return m; }, update: func() { - me.searcher.update(); - foreach (var e; me.list) - e.update(); + if (!me.getVisible()) { + return; + } + # TODO: add options hash processing here + var updater = func { + me.searcher.update(); + foreach (var e; me.list) + e.update(); + } + + if (me.options != nil and me.options['update_wrapper'] !=nil) { + me.options.update_wrapper( me, updater ); # call external wrapper (usually for profiling purposes) + # print("calling update_wrapper!"); + } + else { + # print("not using wrapper"); + updater(); + # debug.dump(me.options); + } + #var start=systime(); + #var end=systime(); + # print(me.type, " layer update:", end-start); + # HACK: hard-coded ... + #setprop("/gui/navdisplay/layers/"~me.type~"/delay-ms", (end-start)*1000 ); }, + ## + # useful to support checkboxes in dialogs (e.g. Map dialog) + # so that we can show/hide layers directly by registering a listener + # TODO: should also allow us to update the navdisplay logic WRT to visibility + hide: func me.group.hide(), + show: func me.group.show(), + getVisible: func me.group.getVisible(), + setVisible: func(visible = 1) me.group.setVisible(visible), del: func() { printlog(_MP_dbg_lvl, "SymbolLayer.del()"); me.controller.del(); @@ -375,17 +483,27 @@ var SymbolLayer = { } return nil; }, - searchCmd: func() me.controller.searchCmd(), + searchCmd: func() { + var result = me.controller.searchCmd(); + # some hardening TODO: don't do this always - only do it once during initialization, i.e. layer creation ? + var type=typeof(result); + if(type != 'nil' and type != 'vector') + die("MapStructure: searchCmd() method MUST return a vector of valid objects or nil! (was:"~type~")"); + return result; + }, # Adds a symbol. - onAdded: func(model) - append(me.list, Symbol.new(me.type, me.group, model)), + onAdded: func(model) { + printlog(_MP_dbg_lvl, "Adding symbol of type "~me.type); + if (model == nil) die("Model was nil for "~debug.string(me.type)); + append(me.list, Symbol.new(me.type, me.group, me, model)); + }, # Removes a symbol. onRemoved: func(model) { - if (me.findsym(model, 1)) die("model not found"); - call(func model.del, nil, var err = []); - # ignore errors - # TODO: ignore only missing member del() errors? and only from the above line? - # Note: die(err[0]) rethrows it; die(err[0]~"") does not. + printlog(_MP_dbg_lvl, "Deleting symbol of type "~me.type); + if (me.findsym(model, 1) == nil) die("model not found"); + call(func model.del(), nil, var err = []); # try... + if (size(err) and err[0] != "No such member: del") # ... and either catch or rethrow + die(err[0]); }, }; # of SymbolLayer @@ -406,10 +524,8 @@ SymbolLayer.Controller = { # @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(); - }, +# Default implementations: + run_update: func() me.layer.update(), # @return List of positioned objects. searchCmd: func() die("searchCmd() not implemented for this SymbolLayer.Controller type!"), @@ -424,6 +540,10 @@ var CompassLayer = { var AltitudeArcLayer = { }; +### +# set up a cache for 32x32 symbols +var SymbolCache32x32 = nil;#SymbolCache.new(1024,32); + var load_MapStructure = func { Map.Controller = { # Static/singleton: @@ -436,8 +556,41 @@ var load_MapStructure = func { 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), + new: func(type, map, arg...) { + var m = call((var class = me.get(type)).new, [map]~arg, class); + if (!contains(m, "map")) + m.map = map; + # FIXME: fails on no member + elsif (m.map != map and !isa(m.map, map) and ( + m.get_position != Map.Controller.get_position + or m.query_range != Map.Controller.query_range + or m.in_range != Map.Controller.in_range)) + { die("m must store the map handle as .map if it uses the default method(s)"); } + }, + # Default implementations: + get_position: func() { + debug.warn("get_position is deprecated"); + return me.map.getLatLon()~[me.map.getAlt()]; + }, + query_range: func() { + debug.warn("query_range is deprecated"); + return me.map.getRange() or 30; + }, + in_range: func(lat, lon, alt=0) { + var range = me.map.getRange(); + if(range == nil) die("in_range: Invalid query range!"); + # print("Query Range is:", range ); + if (lat == nil or lon == nil) die("in_range: lat/lon invalid"); + var pos = geo.Coord.new(); + pos.set_latlon(lat, lon, alt or 0); + var map_pos = me.map.getPosCoord(); + if (map_pos == nil) + return 0; # should happen *ONLY* when map is uninitialized + var distance_m = pos.distance_to( map_pos ); + var is_in_range = distance_m < range * NM2M; + # print("Distance:",distance_m*M2NM," nm in range check result:", is_in_range); + return is_in_range; + }, }; ####### LOAD FILES ####### @@ -448,7 +601,7 @@ var load_MapStructure = func { #print(file); if (name == nil) var name = split("/", file)[-1]; - if (substr(name, size(name)-4) == ".draw") + if (substr(name, size(name)-4) == ".draw") # we don't need this anylonger, right ? name = substr(name, 0, size(name)-5); #print("reading file"); var code = io.readfile(file); @@ -469,63 +622,118 @@ var load_MapStructure = func { return; } #print("calling code"); + call(code, nil, nil, var hash = {}); - #debug.dump(keys(hash)); + + + + # validate + var url = ' http://wiki.flightgear.org/MapStructure#'; + # TODO: these rules should be extended for all main files lcontroller/scontroller and symbol + # TODO move this out of here, so that we can use these checks in other places (i.e. searchCmd validation) + var checks = [ + { extension:'symbol', symbol:'update', type:'func', error:' update() must not be overridden:', id:300}, + { extension:'symbol', symbol:'draw', type:'func', required:1, error:' symbol files need to export a draw() routine:', id:301}, + { extension:'lcontroller', symbol:'searchCmd', type:'func', required:1, error:' lcontroller without searchCmd method:', id:100}, + ]; + + + var makeurl = func(scope, id) url ~ scope ~ ':' ~ id; + var bailout = func(file, message, scope, id) die(file~message~"\n"~makeurl(scope,id) ); + + var current_ext = split('.', file)[-1]; + foreach(var check; checks) { + # check if we have any rules matching the current file extension + if (current_ext == check.extension) { + # check for fields that must not be overridden + if (check['error'] != nil and + hash[check.symbol]!=nil and !check['required'] and + typeof(hash[check.symbol])==check.type ) { + bailout(file,check.error,check.extension,check.id); + } + + # check for required fields + if (check['required'] != nil and + hash[check.symbol]==nil and + typeof( hash[check.symbol]) != check.type) { + bailout(file,check.error,check.extension,check.id); + } + } + } + if(file==FG_ROOT~'/Nasal/canvas/map/DME.scontroller') { + # var test = hash.new(nil); + # debug.dump( id(hash.new) ); + } + # TODO: call self tests/sanity checks here + # and consider calling .symbol::draw() to ensure that certain APIs are NOT used, such as setGeoPosition() and setColor() etc (styling) + return hash; }; + # sets up a shared symbol cache, which will be used by all MapStructure maps and layers + # TODO: need to encode styling information as part of the keys/hash lookup, name - so that + # different maps/layers do not overwrite symbols accidentally + # + canvas.SymbolCache32x32 = SymbolCache.new(1024,32); + var load_deps = func(name) { - load(FG_ROOT~"/Nasal/canvas/map/"~name~".lcontroller", name); - load(FG_ROOT~"/Nasal/canvas/map/"~name~".symbol", name); - load(FG_ROOT~"/Nasal/canvas/map/"~name~".scontroller", name); + load(FG_ROOT~"/Nasal/canvas/map/"~name~".lcontroller", name); + load(FG_ROOT~"/Nasal/canvas/map/"~name~".symbol", name); + load(FG_ROOT~"/Nasal/canvas/map/"~name~".scontroller", name); } - foreach( var name; ['VOR','FIX','NDB','DME','WPT','TFC'] ) + # add your own MapStructure layers here, see the wiki for details: http://wiki.flightgear.org/MapStructure + foreach( var name; ['APT','VOR','FIX','NDB','DME','WPT','TFC','APS',] ) load_deps( name ); load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", name); - ### - # set up a cache for 32x32 symbols - var SymbolCache32x32 = SymbolCache.new(1024,32); + # disable this for now + if(0) { + var drawVOR = func(color, width=3) return func(group) { + # print("drawing vor"); + var bbox = group.createChild("path") + .moveTo(-15,0) + .lineTo(-7.5,12.5) + .lineTo(7.5,12.5) + .lineTo(15,0) + .lineTo(7.5,-12.5) + .lineTo(-7.5,-12.5) + .close() + .setStrokeLineWidth(width) + .setColor( color ); + # debug.dump( bbox.getBoundingBox() ); + }; - var drawVOR = func(color, width=3) return func(group) { - # print("drawing vor"); - var bbox = group.createChild("path") - .moveTo(-15,0) - .lineTo(-7.5,12.5) - .lineTo(7.5,12.5) - .lineTo(15,0) - .lineTo(7.5,-12.5) - .lineTo(-7.5,-12.5) - .close() - .setStrokeLineWidth(width) - .setColor( color ); - # debug.dump( bbox.getBoundingBox() ); - }; + var cachedVOR1 = SymbolCache32x32.add( "VOR-BLUE", drawVOR( color:[0, 0.6, 0.85], width:3), SymbolCache.DRAW_CENTERED ); + var cachedVOR2 = SymbolCache32x32.add( "VOR-RED" , drawVOR( color:[1.0, 0, 0], width: 3), SymbolCache.DRAW_CENTERED ); + var cachedVOR3 = SymbolCache32x32.add( "VOR-GREEN" , drawVOR( color:[0, 1, 0], width: 3), SymbolCache.DRAW_CENTERED ); + var cachedVOR4 = SymbolCache32x32.add( "VOR-WHITE" , drawVOR( color:[1, 1, 1], width: 3), SymbolCache.DRAW_CENTERED ); - var cachedVOR1 = SymbolCache32x32.add( "VOR-BLUE", drawVOR( color:[0, 0.6, 0.85], width:3), SymbolCache.DRAW_CENTERED ); - var cachedVOR2 = SymbolCache32x32.add( "VOR-RED" , drawVOR( color:[1.0, 0, 0], width: 3), SymbolCache.DRAW_CENTERED ); - var cachedVOR3 = SymbolCache32x32.add( "VOR-GREEN" , drawVOR( color:[0, 1, 0], width: 3), SymbolCache.DRAW_CENTERED ); - var cachedVOR4 = SymbolCache32x32.add( "VOR-WHITE" , drawVOR( color:[1, 1, 1], width: 3), SymbolCache.DRAW_CENTERED ); + # visually verify VORs were placed: + # var dlg2 = canvas.Window.new([1024,1024], "dialog"); + # dlg2.setCanvas(SymbolCache32x32.canvas_texture); + + # use one: + # var dlg = canvas.Window.new([120,120],"dialog"); + # var my_canvas = dlg.createCanvas().setColorBackground(1,1,1,1); + # var root = my_canvas.createGroup(); + + # SymbolCache32x32.get(name:"VOR-RED").render( group: root ).setTranslation(60,60); + } # STRESS TEST if (0) { - for(var i=0;i <= 1024/32*4 - 4; i+=1) - SymbolCache32x32.add( "VOR-YELLOW"~i , drawVOR( color:[1, 1, 0], width: 3) ); - - var dlg = canvas.Window.new([640,320],"dialog"); - var my_canvas = dlg.createCanvas().setColorBackground(1,1,1,1); - var root = my_canvas.createGroup(); - - SymbolCache32x32.get(name:"VOR-BLUE").render( group: root ).setGeoPosition(getprop("/position/latitude-deg"),getprop("/position/longitude-deg")); + #for(var i=0;i <= 1024/32*4 - 4; i+=1) + # SymbolCache32x32.add( "VOR-YELLOW"~i , drawVOR( color:[1, 1, 0], width: 3) ); } })(); #print("finished loading files"); ####### TEST SYMBOL ####### - canvas.load_MapStructure = func; + canvas.load_MapStructure = func; # @Philosopher: is this intended/needed ?? }; # load_MapStructure -setlistener("/nasal/canvas/loaded", load_MapStructure); # end ugly module init listener hack +setlistener("/nasal/canvas/loaded", load_MapStructure); # end ugly module init listener hack. FIXME: do smart Nasal bootstrapping, quod est callidus! +# Actually, it would be even better to support reloading MapStructure files, and maybe even MapStructure itself by calling the dtor/del method for each Map and then re-running the ctor diff --git a/Nasal/canvas/api.nas b/Nasal/canvas/api.nas index a1f346292..815980ce5 100644 --- a/Nasal/canvas/api.nas +++ b/Nasal/canvas/api.nas @@ -244,6 +244,7 @@ var Element = { # # @param color Vector of 3 or 4 values in [0, 1] setColorFill: func me.set('fill', _getColor(arg)), + getColorFill: func me.get('fill'), # getTransformedBounds: func me.getTightBoundingBox(), # Calculate the transformation center based on bounding box and center-offset @@ -420,7 +421,7 @@ var Map = { df_controller: nil, new: func(ghost) { - return { parents: [Map, Group.new(ghost)], layers:{} }.setController(); + return { parents: [Map, Group.new(ghost)], layers:{}, controller:nil }.setController(); }, del: func() { @@ -437,6 +438,7 @@ var Map = { }, setController: func(controller=nil) { + if (me.controller != nil) me.controller.del(me); if (controller == nil) controller = Map.df_controller; elsif (typeof(controller) != 'hash') @@ -447,17 +449,24 @@ var Map = { } else { if (!isa(controller, Map.Controller)) die("OOP error: controller needs to inherit from Map.Controller"); - me.controller = controller.new(me); - if (!isa(me.controller, controller)) - die("OOP error: created instance needs to inherit from specific controller class"); + me.controller = call(func controller.new(me), nil, var err=[]); # try... + if (size(err)) { + if (err[0] != "No such member: new") # ... and either catch or rethrow + die(err[0]); + else + me.controller = controller; + } elsif (me.controller == nil) { + me.controller = controller; + } elsif (me.controller != controller and !isa(me.controller, controller)) + die("OOP error: created instance needs to inherit from or be the specific controller class"); } return me; }, - addLayer: func(factory, type_arg=nil, priority=nil) + addLayer: func(factory, type_arg=nil, priority=nil, style=nil, options=nil) { if(contains(me.layers, type_arg)) - print("addLayer() warning: overwriting existing layer:", type_arg); + printlog("warn", "addLayer() warning: overwriting existing layer:", type_arg); # print("addLayer():", type_arg); @@ -466,30 +475,70 @@ var Map = { var type = factory.get(type_arg); else var type = factory; - me.layers[type_arg]= type.new(me); + me.layers[type_arg] = type.new(group:me, map:me, style:style,options:options); if (priority == nil) priority = type.df_priority; if (priority != nil) - me.layers[type_arg].setInt("z-index", priority); + me.layers[type_arg].group.setInt("z-index", priority); + return me; }, getLayer: func(type_arg) me.layers[type_arg], - setPos: func(lat, lon, hdg=nil, range=nil) + + setRange: func(range) me.set("range",range), + getRange: func me.get('range'), + + setPos: func(lat, lon, hdg=nil, range=nil, alt=nil) { me.set("ref-lat", lat); me.set("ref-lon", lon); if (hdg != nil) me.set("hdg", hdg); if (range != nil) - me.set("range", range); + me.setRange(range); + if (alt != nil) + me.set("altitude", hdg); + }, + getPos: func + { + return [me.get("ref-lat"), + me.get("ref-lon"), + me.get("hdg"), + me.get("range"), + me.get("altitude")]; + }, + getLat: func me.get("ref-lat"), + getLon: func me.get("ref-lon"), + getHdg: func me.get("hdg"), + getAlt: func me.get("altitude"), + getRange: func me.get("range"), + getLatLon: func [me.get("ref-lat"), me.get("ref-lon")], + getPosCoord: func + { + var (lat, lon) = (me.get("ref-lat"), + me.get("ref-lon")); + var alt = me.get("altitude"); + if (lat == nil or lon == nil) { + if (contains(me, "coord")) { + debug.warn("canvas.Map: lost ref-lat and/or ref-lon source"); + } + return nil; + } + if (!contains(me, "coord")) { + me.coord = geo.Coord.new(); + } + me.coord.set_latlon(lat,lon,alt or 0); + return me.coord; }, # Update each layer on this Map. Called by # me.controller. - update: func + update: func(predicate=nil) { foreach (var l; keys(me.layers)) { var layer = me.layers[l]; - call(layer.update, arg, layer); + # Only update if the predicate allows + if (predicate == nil or predicate(layer)) + call(layer.update, arg, layer); } return me; }, @@ -570,7 +619,10 @@ var Text = { me.setDouble("max-width", w); }, setColor: func me.set('fill', _getColor(arg)), - setColorFill: func me.set('background', _getColor(arg)) + getColor: func me.get('fill'), + + setColorFill: func me.set('background', _getColor(arg)), + getColorFill: func me.get('background'), }; # Path @@ -826,8 +878,10 @@ var Path = { }, setColor: func me.setStroke(_getColor(arg)), - setColorFill: func me.setFill(_getColor(arg)), + getColor: func me.getStroke(), + setColorFill: func me.setFill(_getColor(arg)), + getColorFill: func me.getColorFill(), setFill: func(fill) { me.set('fill', fill); @@ -836,6 +890,8 @@ var Path = { { me.set('stroke', stroke); }, + getStroke: func me.get('stroke'), + setStrokeLineWidth: func(width) { me.setDouble('stroke-width', width); @@ -997,6 +1053,7 @@ var Canvas = { # # @param color Vector of 3 or 4 values in [0, 1] setColorBackground: func () { me.texture.getNode('background', 1).setValue(_getColor(arg)); me; }, + getColorBackground: func me.texture.get('background'), # Get path of canvas to be used eg. in Image::setFile getPath: func() { diff --git a/Nasal/canvas/map/APS.lcontroller b/Nasal/canvas/map/APS.lcontroller new file mode 100644 index 000000000..f39224be7 --- /dev/null +++ b/Nasal/canvas/map/APS.lcontroller @@ -0,0 +1,27 @@ +# See: http://wiki.flightgear.org/MapStructure +# Class things: +var name = 'APS'; +var parents = [SymbolLayer.Controller]; +var __self__ = caller(0)[0]; +SymbolLayer.Controller.add(name, __self__); +SymbolLayer.add(name, { + parents: [SymbolLayer], + type: name, # Symbol type + df_controller: __self__, # controller to use by default -- this one +}); +var new = func(layer) { + layer.searcher._equals = func(a,b) { + a == b; + } + return { + parents: [__self__], + map: layer.map, + }; +}; +var del = func; + +var searchCmd = func { + var c = me.map.getPosCoord(); + return [c]; +}; + diff --git a/Nasal/canvas/map/APS.scontroller b/Nasal/canvas/map/APS.scontroller new file mode 100644 index 000000000..733e26db7 --- /dev/null +++ b/Nasal/canvas/map/APS.scontroller @@ -0,0 +1,9 @@ +# See: http://wiki.flightgear.org/MapStructure +# Class things: +var name = 'APS'; +var parents = [Symbol.Controller]; +var __self__ = caller(0)[0]; +Symbol.Controller.add(name, __self__); +Symbol.registry[ name ].df_controller = __self__; +var new = func(model) ; # this controller doesn't need an instance + diff --git a/Nasal/canvas/map/APS.symbol b/Nasal/canvas/map/APS.symbol new file mode 100644 index 000000000..c54749973 --- /dev/null +++ b/Nasal/canvas/map/APS.symbol @@ -0,0 +1,16 @@ +# See: http://wiki.flightgear.org/MapStructure +# Class things: +var name = 'APS'; +var parents = [DotSym]; +var __self__ = caller(0)[0]; +DotSym.makeinstance( name, __self__ ); + +var element_type = "group"; +var element_id = "airplane"; + +var init = func { + canvas.parsesvg(me.element, "Nasal/canvas/map/boeingAirplane.svg"); + me.draw(); +}; +var draw = func me.element.setRotation(me.layer.map.getHdg()); + diff --git a/Nasal/canvas/map/APT.lcontroller b/Nasal/canvas/map/APT.lcontroller new file mode 100644 index 000000000..643cdb484 --- /dev/null +++ b/Nasal/canvas/map/APT.lcontroller @@ -0,0 +1,35 @@ +# See: http://wiki.flightgear.org/MapStructure +# Class things: +var name = 'APT'; +var parents = [SymbolLayer.Controller]; +var __self__ = caller(0)[0]; +SymbolLayer.Controller.add(name, __self__); +SymbolLayer.add(name, { + parents: [SymbolLayer], + type: name, # Symbol type + df_controller: __self__, # controller to use by default -- this one +}); +var a_instance = nil; +var new = func(layer) { + var m = { + parents: [__self__], + layer: layer, + map: layer.map, + listeners: [], + }; + __self__.a_instance = m; + return m; +}; +var del = func() { + #print(name,".lcontroller.del()"); + foreach (var l; me.listeners) + removelistener(l); +}; + +var searchCmd = func { + #print("Running query:", name); + var range = me.map.getRange(); + if (range == nil) return; + return positioned.findAirportsWithinRange(me.map.getPosCoord(), range); +}; + diff --git a/Nasal/canvas/map/APT.scontroller b/Nasal/canvas/map/APT.scontroller new file mode 100644 index 000000000..4269dadbc --- /dev/null +++ b/Nasal/canvas/map/APT.scontroller @@ -0,0 +1,13 @@ +# See: http://wiki.flightgear.org/MapStructure +# Class things: +var name = 'APT'; +var parents = [Symbol.Controller]; +var __self__ = caller(0)[0]; +Symbol.Controller.add(name, __self__); +Symbol.registry[ name ].df_controller = __self__; +var new = func(model) ; # this controller doesn't need an instance +var LayerController = SymbolLayer.Controller.registry[ name ]; +var isActive = func(model) LayerController.a_instance.isActive(model); +var query_range = func() + die( name~".scontroller.query_range /MUST/ be provided by implementation" ); + diff --git a/Nasal/canvas/map/APT.symbol b/Nasal/canvas/map/APT.symbol new file mode 100644 index 000000000..751d159d7 --- /dev/null +++ b/Nasal/canvas/map/APT.symbol @@ -0,0 +1,52 @@ +# See: http://wiki.flightgear.org/MapStructure +# Class things: +var name = 'APT'; +var parents = [DotSym]; +var __self__ = caller(0)[0]; +DotSym.makeinstance( name, __self__ ); + +var element_type = "group"; # we want a group, becomes "me.element" +var icon_fix = nil; +var text_fix = nil; + +# add the draw routine from airports-nd.draw here +var draw = func { + if (me.icon_fix != nil) return; + var icon_apt = me.element.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 = me.element.createChild("text", name ~ " label") + .setDrawMode( canvas.Text.TEXT ) + .setTranslation(17,35) + .setText(me.model.id) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") + .setColor(0,0.6,0.85) + .setFontSize(28); + #me.element.setGeoPosition(lat, lon) + # .set("z-index",1); # FIXME: this needs to be configurable!! + +# disabled: +if(0) { + # the fix symbol + me.icon_fix = me.element.createChild("path") + .moveTo(-15,15) + .lineTo(0,-15) + .lineTo(15,15) + .close() + .setStrokeLineWidth(3) + .setColor(0,0.6,0.85) + .setScale(0.5,0.5); # FIXME: do proper LOD handling here - we need to scale according to current texture dimensions vs. original/design dimensions + # the fix label + me.text_fix = me.element.createChild("text") + .setDrawMode( canvas.Text.TEXT ) + .setText(me.model.id) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") + .setFontSize(28) + .setTranslation(5,25); + } +}; + diff --git a/Nasal/canvas/map/DME.lcontroller b/Nasal/canvas/map/DME.lcontroller index b2d3c714e..0994a71b6 100644 --- a/Nasal/canvas/map/DME.lcontroller +++ b/Nasal/canvas/map/DME.lcontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'DME'; var parents = [SymbolLayer.Controller]; @@ -7,17 +8,39 @@ SymbolLayer.add(name, { parents: [SymbolLayer], type: name, # Symbol type df_controller: __self__, # controller to use by default -- this one + df_style: { + line_width: 3, + scale_factor: 1, + animation_test: 0, + debug: 1, + color_default: [0,0.6,0.85], + color_tuned: [0,1,0], + } }); -var a_instance = nil; var new = func(layer) { var m = { parents: [__self__], layer: layer, + map: layer.map, listeners: [], - query_range_nm: 25, query_type:'dme', }; - __self__.a_instance = m; + ## + # default styling parameters - can be overridden via addLayer( style:{key:value, ...} ) + + if (contains(m.layer,'style')) return m; # we already have a proper style + + # otherwise, set up a default style: + m.layer.style={}; + m.layer.style.debug = 0; # HACK: setting this enables benchmarking and printlog statements + m.layer.style.animation_test = 0; + + # these are used by the draw() routines, see DME.symbol + m.layer.style.scale_factor = 1.0 ; # applied to the whole group for now + m.layer.style.line_width = 3.0; + m.layer.style.color_tuned = [0,1,0]; + m.layer.style.color_default = [0,0.6,0.85]; + return m; }; var del = func() { @@ -26,7 +49,9 @@ var del = func() { }; var searchCmd = func { - #print("Running query:", me.query_type); - return positioned.findWithinRange(me.query_range_nm, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch + printlog(_MP_dbg_lvl, "Running query:", me.query_type); + var range = me.map.getRange(); + if (range == nil) return; + return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type); }; diff --git a/Nasal/canvas/map/DME.scontroller b/Nasal/canvas/map/DME.scontroller index 0e54d4dc2..a565d51b1 100644 --- a/Nasal/canvas/map/DME.scontroller +++ b/Nasal/canvas/map/DME.scontroller @@ -1,12 +1,9 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'DME'; var parents = [Symbol.Controller]; var __self__ = caller(0)[0]; Symbol.Controller.add(name, __self__); Symbol.registry[ name ].df_controller = __self__; -var new = func(model) ; # this controller doesn't need an instance -var LayerController = SymbolLayer.Controller.registry[ name ]; -var isActive = func(model) LayerController.a_instance.isActive(model); -var is_tuned = func() - die( name~".scontroller.is_tuned /MUST/ be provided by implementation" ); +var new = func(model, symbol) ; # this controller doesn't need an instance diff --git a/Nasal/canvas/map/DME.symbol b/Nasal/canvas/map/DME.symbol index fbf523227..5e685a0ff 100644 --- a/Nasal/canvas/map/DME.symbol +++ b/Nasal/canvas/map/DME.symbol @@ -1,3 +1,15 @@ +# See: http://wiki.flightgear.org/MapStructure +# This layer is currently being restructured to better support 1) styling, 2) LOD and 3) caching of instanced symbols +# The corresponding APIs in MapStructure.nas and api.nas should probably be extended acccordingly +# +# We can look into adapting the other layers once we have a single use-case that works properly - including styling, LOD and caching - +# at that point, the framework should have evolved sufficiently. +# +# It would probably be a good idea to incrementally port some other layers and provide the corresponding helpers/APIs, to reduce the amount +# of specialized/identical code in the .symbol files. +# + + # Class things: var name = 'DME'; var parents = [DotSym]; @@ -5,11 +17,101 @@ var __self__ = caller(0)[0]; DotSym.makeinstance( name, __self__ ); var element_type = "group"; # we want a group, becomes "me.element" + +var timer = nil; + +### +# symbols (canvas groups) managed by this file +# var icon_dme = nil; -var draw = func { - # Init - if (me.icon_dme == nil) { +var scale_animation = 1; + +var animate = func { + if (me.scale_animation >= 1) + me.scale_animation -= .5; + else + me.scale_animation += .5; + me.element.setScale( me.scale_animation ); +} + + +var del = func { + # me.timer.stop(); +} + +# CachedElement +# StyleAttribute +# Styleable +# RangeAwareElement + + + +# var DMEIcon = StyleableElement.new( [{color:IconColor}, ] ); + +### +# helper to tell if the symbol is already initialized or not +# TODO: encapsulate API-wise (this is a very common thing to do...) +var is_initialized = func me.icon_dme != nil; + +### +# FIXME: these should probably be part of MapStructure itself +# TODO: Need to come up with a StyleableElement class for this sort of stuff +var apply_styling = func { + # add all styleable groups here + + # no need to do this whenever draw is called - typically, attributes won't change at all - so this is kinda redundant + var current_color = me.icon_dme.getColor(); + var required_color = nil; + + if (typeof(me.layer.map.controller["is_tuned"]) == 'func' and me.layer.map.controller.is_tuned(me.model.frequency/100)) + #TODO: once we support caching/instancing, we cannot just change the symbol like this - we need to use a different symbol from the cache instead + # which is why these things need to be done ONCE during init to set up a cache entry for each symbol variation to come up with a corresponding raster image + # TODO: API-wise it would make sense to maintain a vector of required keys, so that the style hash can be validated in the ctor of the layer + # to ensure that all mandatory fields are supplied + required_color = canvas._getColor( me.layer.style.color_tuned); + else + required_color = canvas._getColor( me.layer.style.color_default); + + if (current_color != required_color) { + # print("Setting color!"); + # TODO: this is where we would select another cached symbol + me.icon_dme.setColor( required_color ); + } + # else print("Not changing color (unnecessary)"); + + # debug.dump(me.layer.style); +} + + +### +# NOTE: This is only applied during initialization +# TODO: expose via addLayer/style param +# TODO: also needs to be aware of current range, so that proper LOD handling can be implemented +var apply_scale = func { + # add all symbols here that need scaling + # print("Scaling:", me.layer.style.scale_factor); + me.icon_dme.setScale( me.layer.style.scale_factor ); +} + +### +# draw routines must be called here to create a lookup table +# with different symbols, based on styling (colors etc) +# because we can no longer change instanced symbols later on +# as they will be shared, so we need one cache entry for each +# variation in style +var init_cache = func { + +} + +## +# init is done separately to prepare support for caching (instanced symbols) +# NOTE: People should not be "hard-coding" things like color/size here +# these need to be encapsulated via a Hash lookup, so that things can be +# easily customized +# +var init_symbols = func { + # debug.dump(me.layer.style); me.icon_dme = me.element.createChild("path") .moveTo(-15,0) .line(-12.5,-7.5) @@ -25,11 +127,31 @@ var draw = func { .horiz(-14.5) .vert(-14.5) .close() - .setStrokeLineWidth(3); + .setStrokeLineWidth( me.layer.style.line_width ); #TODO: this should be style-able + + # finally scale the symbol as requested, this is done last so that people can override this when creating the layer + me.apply_scale(); + if (me.layer.style.animation_test) { + + me.timer = maketimer(0.33, func me.animate() ); + me.timer.start(); } - if (me.controller != nil and me.controller.is_tuned(me.model.frequency/100)) - me.icon_dme.setColor(0,1,0); - else - me.icon_dme.setColor(0,0.6,0.85); +} + +var updateRun = func { + # check if the current station is tuned or not - and style the symbol accordingly (color) + me.apply_styling(); +} + +## +# this method is REQUIRED (basically, the entry point for drawing - most others are just helpers) +var draw = func { + # print("DME:draw()"); + # Init: will set up the symbol if it isn't already + if ( !me.is_initialized() ) + me.init_symbols(); + + # wrapper for custom styling, based on tuned/default colors (see lookup hash above) + me.updateRun(); }; diff --git a/Nasal/canvas/map/FIX.lcontroller b/Nasal/canvas/map/FIX.lcontroller index ed89fb06b..e549446ad 100644 --- a/Nasal/canvas/map/FIX.lcontroller +++ b/Nasal/canvas/map/FIX.lcontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'FIX'; var parents = [SymbolLayer.Controller]; @@ -7,27 +8,35 @@ SymbolLayer.add(name, { parents: [SymbolLayer], type: name, # Symbol type df_controller: __self__, # controller to use by default -- this one + + df_style: { + line_width: 3, + scale_factor: 0.5, + debug: 1, + color_default: [0, 0.6, 0.85], + } + }); -var a_instance = nil; var new = func(layer) { var m = { parents: [__self__], layer: layer, + map: layer.map, listeners: [], - query_range_nm: 25, query_type:'fix', }; - __self__.a_instance = m; return m; }; var del = func() { - #print("VOR.lcontroller.del()"); + printlog(_MP_dbg_lvl, "VOR.lcontroller.del()"); foreach (var l; me.listeners) removelistener(l); }; var searchCmd = func { - #print("Running query:", me.query_type); - return positioned.findWithinRange(me.query_range_nm, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch + printlog(_MP_dbg_lvl, "Running query:", me.query_type); + var range = me.map.getRange(); + if (range == nil) return; + return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type); }; diff --git a/Nasal/canvas/map/FIX.scontroller b/Nasal/canvas/map/FIX.scontroller index 5726f86ab..0e54a5d98 100644 --- a/Nasal/canvas/map/FIX.scontroller +++ b/Nasal/canvas/map/FIX.scontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'FIX'; var parents = [Symbol.Controller]; @@ -5,8 +6,4 @@ var __self__ = caller(0)[0]; Symbol.Controller.add(name, __self__); Symbol.registry[ name ].df_controller = __self__; var new = func(model) ; # this controller doesn't need an instance -var LayerController = SymbolLayer.Controller.registry[ name ]; -var isActive = func(model) LayerController.a_instance.isActive(model); -var query_range = func() - die( name~".scontroller.query_range /MUST/ be provided by implementation" ); diff --git a/Nasal/canvas/map/FIX.symbol b/Nasal/canvas/map/FIX.symbol index c2eee2c11..afb34099c 100644 --- a/Nasal/canvas/map/FIX.symbol +++ b/Nasal/canvas/map/FIX.symbol @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'FIX'; var parents = [DotSym]; @@ -8,23 +9,41 @@ var element_type = "group"; # we want a group, becomes "me.element" var icon_fix = nil; var text_fix = nil; -var draw = func { - if (me.icon_fix != nil) return; - # the fix symbol - me.icon_fix = me.element.createChild("path") +## +# used during initialization to populate the symbol cache with a FIX symbol +# +var drawFIX = func(color, width) func(group) { + + var symbol = group.createChild("path") .moveTo(-15,15) .lineTo(0,-15) .lineTo(15,15) .close() - .setStrokeLineWidth(3) - .setColor(0,0.6,0.85) + .setStrokeLineWidth(width) + .setColor(color) .setScale(0.5,0.5); # FIXME: do proper LOD handling here - we need to scale according to current texture dimensions vs. original/design dimensions - # the fix label + return symbol; +} + +var icon_fix_cached = [ + SymbolCache32x32.add( + name: "FIX", + callback: drawFIX( color:[0, 0.6, 0.85], width:3 ), # TODO: use the style hash to encapsulate styling stuff + draw_mode: SymbolCache.DRAW_CENTERED + ) +]; + +var draw = func { + if (me.icon_fix != nil) return; # fix icon already initialized + # initialize the fix symbol + me.icon_fix = icon_fix_cached[0].render(me.element); + + # non-cached stuff: + # FIXME: labels need to be LOD-aware (i.e. aware of MapController range, so that we can hide/show them) me.text_fix = me.element.createChild("text") .setDrawMode( canvas.Text.TEXT ) .setText(me.model.id) - .setFont("LiberationFonts/LiberationSans-Regular.ttf") - .setFontSize(28) + .setFont("LiberationFonts/LiberationSans-Regular.ttf") # TODO: encapsulate styling stuff + .setFontSize(28) # TODO: encapsulate styling stuff .setTranslation(5,25); -}; - +} diff --git a/Nasal/canvas/map/NDB.lcontroller b/Nasal/canvas/map/NDB.lcontroller index 3250fc0f7..1eee1a3a0 100644 --- a/Nasal/canvas/map/NDB.lcontroller +++ b/Nasal/canvas/map/NDB.lcontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'NDB'; var parents = [SymbolLayer.Controller]; @@ -12,8 +13,8 @@ var new = func(layer) { var m = { parents: [__self__], layer: layer, + map: layer.map, listeners: [], - query_range_nm: 25, query_type:'ndb', }; return m; @@ -24,7 +25,9 @@ var del = func() { }; var searchCmd = func { - #print("Running query:", me.query_type); - return positioned.findWithinRange(me.query_range_nm, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch + printlog(_MP_dbg_lvl, "Running query:", me.query_type); + var range = me.map.getRange(); + if (range == nil) return; + return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type); }; diff --git a/Nasal/canvas/map/NDB.scontroller b/Nasal/canvas/map/NDB.scontroller index 7d1011db0..255a24313 100644 --- a/Nasal/canvas/map/NDB.scontroller +++ b/Nasal/canvas/map/NDB.scontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'NDB'; var parents = [Symbol.Controller]; @@ -6,7 +7,4 @@ Symbol.Controller.add(name, __self__); Symbol.registry[ name ].df_controller = __self__; var new = func(model) ; # this controller doesn't need an instance var LayerController = SymbolLayer.Controller.registry[ name ]; -var isActive = func(model) LayerController.a_instance.isActive(model); -var query_range = func() - die( name~".scontroller.query_range /MUST/ be provided by implementation" ); diff --git a/Nasal/canvas/map/NDB.symbol b/Nasal/canvas/map/NDB.symbol index 541b4cebb..8154ee9e4 100644 --- a/Nasal/canvas/map/NDB.symbol +++ b/Nasal/canvas/map/NDB.symbol @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'NDB'; var parents = [DotSym]; diff --git a/Nasal/canvas/map/TFC.lcontroller b/Nasal/canvas/map/TFC.lcontroller index 41767e876..368761233 100644 --- a/Nasal/canvas/map/TFC.lcontroller +++ b/Nasal/canvas/map/TFC.lcontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'TFC'; var parents = [SymbolLayer.Controller]; @@ -7,6 +8,7 @@ SymbolLayer.add(name, { parents: [SymbolLayer], type: name, # Symbol type df_controller: __self__, # controller to use by default -- this one + df_style: { debug: 0 }, # style to use by default }); var model_root = props.globals.getNode("/ai/models/"); @@ -15,30 +17,10 @@ var new = func(layer) { var m = { parents: [__self__], layer: layer, + map: layer.map, listeners: [], - query_range_nm: 25, + searchCmd: searchCmd_default, }; - # Listen to ai model events - append(m.listeners, setlistener( - model_root.getNode("model-added"), func(n) { - #printlog(_MP_dbg_lvl, "Dynamically adding model at "~n.getValue()); - var node = props.globals.getNode(n.getValue()); - var name = node.getName(); - if (name == "aircraft" or name == "multiplayer") - if (m.in_range(node.getValue("position/latitude-deg"), node.getValue("position/longitude-deg"))) - layer.onAdded(TrafficModel.new(node)); - } - )); - append(m.listeners, setlistener( - model_root.getNode("model-removed"), func(n) { - #printlog(_MP_dbg_lvl, "Dynamically deleting model at "~n.getValue()); - var node = props.globals.getNode(n.getValue()); - var name = node.getName(); - if (name == "aircraft" or name == "multiplayer") - if (m.in_range(node.getValue("position/latitude-deg"), node.getValue("position/longitude-deg"))) - layer.onRemoved(TrafficModel.new(node)); - } - )); layer.searcher._equals = func(l,r) l.equals(r); return m; }; @@ -47,19 +29,6 @@ var del = func() { foreach (var l; me.listeners) removelistener(l); }; -var in_range = func(lat,lon,myPositionVec=nil,max_dist_m=nil) { - if (lat == nil or lon == nil) return 0; - var pos = geo.Coord.new(); - pos.set_latlon(lat,lon); - var myPosition = geo.Coord.new(); - # FIXME: need a Map Controller for this, and all query_range's/get_position's - if (myPositionVec == nil) - var myPositionVec = me.get_position(); - myPosition.set_latlon( myPositionVec[0], myPositionVec[1]); - if (max_dist_m == nil) - var max_dist_m = me.query_range()*NM2M; - return (pos.distance_to( myPosition ) <= max_dist_m ) -}; var TrafficModel = { new: func(node, id=nil, layer=nil) { @@ -72,43 +41,51 @@ var TrafficModel = { node: node, pos: node.getNode("position",1), }; - if (m.pos == nil) - m.latlon = func [nil,nil,nil]; - #debug.dump(m); # why doesn't this print? return m; }, equals: func(other) other.id == me.id, - latlon: func() { + latlon: func() { # this makes sure to look like a geo.Coord to MapStructure, but will internally use the AI/MP traffic properties instead return [ me.pos.getValue("latitude-deg"), me.pos.getValue("longitude-deg"), me.pos.getValue("altitude-ft") ]; }, + # these are helper methods related to TCAS handling (TAs/RAs) + get_threat_lvl: func() me.getValue("tcas/threat-level"), + get_vspd: func() (me.getValue("velocities/vertical-speed-fps") or 0)*60, + get_alt: func() (me.getValue("position/altitude-ft") or 0), }; -var searchCmd = func { + +## +# dummy/placeholder (will be overridden in ctor and set to the default callback) +var searchCmd = func; + +var searchCmd_default = func { # TODO: this would be a good candidate for splitting across frames - #print("Doing query: "~name); + printlog(_MP_dbg_lvl, "Doing query: "~name); var result = []; - # FIXME: need a Map Controller for this, and all query_range's/get_position's - var myPositionVec = me.get_position(); - var max_dist_m = me.query_range()*NM2M; + var models = 0; # AI and Multiplayer traffic - foreach (var traffic; [model_root.getChildren("aircraft"), model_root.getChildren("multiplayer")]) { - foreach(var t; traffic) { - if (me.in_range(t.getValue("position/latitude-deg"), - t.getValue("position/longitude-deg"), - myPositionVec, - max_dist_m)) - append(result, TrafficModel.new(t, nil, me.layer)); + foreach (var t; model_root.getChildren()) { + if (!t.getValue("valid")) continue; + var t_id = t.getNode("id"); + if (t_id == nil or t_id.getValue() == -1) continue; + models += 1; + var (lat,lon) = (t.getValue("position/latitude-deg"), + t.getValue("position/longitude-deg")); + if (lat == nil or lon == nil) { + printlog("alert", "lat/lon was nil for AI node "~t.getPath()); + continue; } + if (me.map.controller.in_range(lat, lon)) + append(result, TrafficModel.new(t, nil, me.layer)); } - #debug.dump(result); - #return []; + #print("Found "~size(result)~" TrafficModel's in range out of "~models~" total."); return result; }; diff --git a/Nasal/canvas/map/TFC.scontroller b/Nasal/canvas/map/TFC.scontroller index a36364aec..09e818891 100644 --- a/Nasal/canvas/map/TFC.scontroller +++ b/Nasal/canvas/map/TFC.scontroller @@ -1,12 +1,16 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'TFC'; var parents = [Symbol.Controller]; var __self__ = caller(0)[0]; Symbol.Controller.add(name, __self__); Symbol.registry[name].df_controller = __self__; -var new = func(model) ; # this controller doesn't need an instance -# XXX: this is more model-ish than controller-ish -var get_threat_lvl = func(model) model.getValue("tcas/threat-level"); -var get_vspd = func(model) (model.getValue("velocities/vertical-speed-fps") or 0)*60; -var get_alt_diff = func(model) (model.getValue("position/altitude-ft") or 0) - (getprop("/position/altitude-ft") or 0); +var new = func(model, symbol) ; # this controller doesn't need an instance +var get_alt_diff = func(model) { + # debug.dump( keys(me) ); + var model_alt = model.get_alt(); + var alt = getprop("/position/altitude-ft"); # FIXME: hardcoded - right, we should probably generalize the "NDSourceDriver logic found in navdisplay.mfd and make it part of MapStructure + if (alt == nil or model_alt == nil) return 0; + return alt-model_alt; +}; diff --git a/Nasal/canvas/map/TFC.symbol b/Nasal/canvas/map/TFC.symbol index 25fc56dad..70b9c563c 100644 --- a/Nasal/canvas/map/TFC.symbol +++ b/Nasal/canvas/map/TFC.symbol @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'TFC'; var parents = [DotSym]; @@ -13,15 +14,15 @@ var arrow_type = nil; var draw_tcas_arrow = nil; var draw = func { - if (me.draw_tcas_arrow == nil) - me.draw_tcas_arrow = [ + if (draw_tcas_arrow == nil) + draw_tcas_arrow = [ draw_tcas_arrow_above_500, draw_tcas_arrow_below_500 ]; #var callsign = me.model.getNode("callsign").getValue(); # print("Drawing traffic for:", callsign ); - var threatLvl = me.controller.get_threat_lvl(me.model); - var vspeed = me.controller.get_vspd(me.model); + var threatLvl = me.model.get_threat_lvl(); + var vspeed = me.model.get_vspd(); var altDiff = me.controller.get_alt_diff(me.model); # Init if (me.text_tcas == nil) { @@ -59,7 +60,7 @@ var draw = func { .setColor(1,0,0) .setColorFill(1,0,0); me.text_tcas.setColor(1,0,0); - me.arrow_tcas.setColor(1,0,0); + me.arrow_tcas[me.arrow_type].setColor(1,0,0); } elsif (threatLvl == 2) { # traffic advisory me.icon_tcas.moveTo(-17,0) @@ -68,7 +69,7 @@ var draw = func { .setColor(1,0.5,0) .setColorFill(1,0.5,0); me.text_tcas.setColor(1,0.5,0); - me.arrow_tcas.setColor(1,0.5,0); + me.arrow_tcas[me.arrow_type].setColor(1,0.5,0); } elsif (threatLvl == 1) { # proximate traffic me.icon_tcas.moveTo(-10,0) @@ -78,6 +79,8 @@ var draw = func { .close() .setColor(1,1,1) .setColorFill(1,1,1); + me.text_tcas.setColor(1,1,1); + me.arrow_tcas[me.arrow_type].setColor(1,1,1); } else { # other traffic me.icon_tcas.moveTo(-10,0) @@ -86,6 +89,8 @@ var draw = func { .lineTo(0,17) .close() .setColor(1,1,1); + me.text_tcas.setColor(1,1,1); + me.arrow_tcas[me.arrow_type].setColor(1,1,1); } }; diff --git a/Nasal/canvas/map/VOR.lcontroller b/Nasal/canvas/map/VOR.lcontroller index e7adb3d2a..c0edca10e 100644 --- a/Nasal/canvas/map/VOR.lcontroller +++ b/Nasal/canvas/map/VOR.lcontroller @@ -1,17 +1,29 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: +var name ='VOR'; var parents = [SymbolLayer.Controller]; var __self__ = caller(0)[0]; -SymbolLayer.Controller.add("VOR", __self__); -SymbolLayer.add("VOR", { +SymbolLayer.Controller.add(name, __self__); +SymbolLayer.add(name, { parents: [SymbolLayer], - type: "VOR", # Symbol type + type: name, # Symbol type df_controller: __self__, # controller to use by default -- this one }); -var a_instance = nil; var new = func(layer) { + +if(0) { + # TODO: generalize and move to MapStructure.nas + var required_controllers = [ {name: 'query_range',type:'func'}, ]; + foreach(var c; required_controllers) { + if (!contains(layer.map.controller, c.name) or typeof(layer.map.controller[c.name]) !=c.type) + die("Required controller is missing/invalid:"~ c.name ~ ' ['~c.type~']' ); + } +} + var m = { parents: [__self__], layer: layer, + map: layer.map, active_vors: [], navNs: props.globals.getNode("instrumentation").getChildren("nav"), listeners: [], @@ -26,11 +38,10 @@ var new = func(layer) { } #call(debug.dump, keys(layer)); m.changed_freq(update:0); - __self__.a_instance = m; return m; }; var del = func() { - printlog(_MP_dbg_lvl, "VOR.lcontroller.del()"); + printlog(_MP_dbg_lvl, name,".lcontroller.del()"); foreach (var l; me.listeners) removelistener(l); }; @@ -50,6 +61,8 @@ var changed_freq = func(update=1) { }; var searchCmd = func { printlog(_MP_dbg_lvl, "Running query:", me.query_type); - return positioned.findWithinRange(100, me.query_type); # the range should also be exposed, it will typically be controlled via a GUI widget or NavDisplay switch + var range = me.map.getRange(); + if (range == nil) return; + return positioned.findWithinRange(me.map.getPosCoord(), range, me.query_type); }; diff --git a/Nasal/canvas/map/VOR.scontroller b/Nasal/canvas/map/VOR.scontroller index 9b07c3d43..ec14ce929 100644 --- a/Nasal/canvas/map/VOR.scontroller +++ b/Nasal/canvas/map/VOR.scontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'VOR'; var parents = [Symbol.Controller]; @@ -5,8 +6,4 @@ var __self__ = caller(0)[0]; Symbol.Controller.add(name, __self__); Symbol.registry[name].df_controller = __self__; var new = func(model) ; # this controller doesn't need an instance -var LayerController = SymbolLayer.Controller.registry[name]; -var isActive = func(model) LayerController.a_instance.isActive(model); -var query_range = func() - die(name~".scontroller.query_range /MUST/ be provided by implementation"); diff --git a/Nasal/canvas/map/VOR.symbol b/Nasal/canvas/map/VOR.symbol index faf37c206..1b30ee0c4 100644 --- a/Nasal/canvas/map/VOR.symbol +++ b/Nasal/canvas/map/VOR.symbol @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'VOR'; var parents = [DotSym]; @@ -5,28 +6,102 @@ var __self__ = caller(0)[0]; DotSym.makeinstance( name, __self__ ); var element_type = "group"; # we want a group, becomes "me.element" -var icon_vor = nil; +var icon_vor = nil; # a handle to the cached icon 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 active = -1; + +### +# this function returns a new function that renders the symbol +# into a canvas group +# the signature of the first func should contain all customizable +# parameters (such as color/width etc) +# +# you will typically have one such function for each cacheable symbol +# and only call it once during initialization (once for each cached style/variation) +# the signature of the 2nd func remains untouched, it's internally used +# +# NOTE: callbacks that create symbols for caching MUST render them using a +# transparent background !! +# +var drawVOR = func(color, width=3) return func(group) { + + # init_calls += 1; # TODO: doing this here is too fragile, this should be done by MapStructure ... + # if( init_calls >= 2) + # print("Warning: draw() routine for a cached element has been called more than once, should only happen during reset/reinit"); + + # print("drawing vor"); + var symbol = group.createChild("path") + .moveTo(-15,0) + .lineTo(-7.5,12.5) + .lineTo(7.5,12.5) + .lineTo(15,0) + .lineTo(7.5,-12.5) + .lineTo(-7.5,-12.5) + .close() + .setStrokeLineWidth(width) + .setColor( color ); + + return symbol; # make this explicit, not everybody knows Nasal internals inside/out ... +}; + +## +# a vector with pre-created symbols that are cached during initialization +# this needs to be done for each variation of each symbol, i.e. color/width etc +# note that scaling should not be done here, likewise for positioning via setGeoPosition() +# +# FIXME: We still need to encode styling information as part of the key/name lookup +# so that the cache entry's name contains styling info, or different maps/layers may +# overwrite their symbols +# +# icon_vor_cache[0] - inactive symbol +# icon_vor_cache[1] - active symbol +var icon_vor_cached = [ + SymbolCache32x32.add( + name: "VOR-INACTIVE", + callback: drawVOR( color:[0, 0.6, 0.85], width:3 ), + draw_mode: SymbolCache.DRAW_CENTERED + ), + SymbolCache32x32.add( + name: "VOR-ACTIVE", + callback: drawVOR( color:[0, 1, 0], width:3 ), + draw_mode: SymbolCache.DRAW_CENTERED + ), +]; + +## +# TODO: make this a part of the framework, for use by other layers/symbols etc +# +var controller_check = func(layer, controller, arg...) { + var ret = call(compile("call(layer.controller."~controller~", arg, layer.controller)", ""), arg, var err=[]); + + if (size(err)) + if (err[0] == "No such member: "~controller) + print("MapStructure Warning: Required controller not found: ", name, ":", controller); + else die(err[0]); # rethrow + + return ret; # nil if not found +} var draw = func { # Init - if (me.icon_vor == nil) { - me.icon_vor = me.element.createChild("path") - .moveTo(-15,0) - .lineTo(-7.5,12.5) - .lineTo(7.5,12.5) - .lineTo(15,0) - .lineTo(7.5,-12.5) - .lineTo(-7.5,-12.5) - .close() - .setStrokeLineWidth(3) - .setColor(0,0.6,0.85); + if (0 and me.model.id == "SAC") # hack to test isActive() around KSMF + setprop("instrumentation/nav[0]/frequencies/selected-mhz", me.model.frequency/100); + var active = controller_check(me.layer, 'isActive', me.model); + #print(me.model.id,"/", me.model.frequency/100, " isActive:", active); + if (active != me.active) { + #print("VOR.symbol: active != me.active: del/render event triggered"); + if (me.icon_vor != nil) me.icon_vor.del(); + # look up the correct symbol from the cache and render it into the group as a raster image + me.icon_vor = icon_vor_cached[active or 0].render(me.element); + me.active = active; # update me.active flag } - # Update - if (me.controller.isActive(me.model)) { - if (me.range_vor == nil) { - var rangeNm = me.controller.query_range(); + # Update (also handles non-cached stuff, such as text labels or animations) + # TODO: we can use a func generator to pre-create the callback/data structure for this + if (active) { + if (me.range_vor == nil) { + # initialize me.range and me.radial_vor + var rangeNm = me.layer.map.getRange(); # print("VOR is tuned:", me.model.id); var radius = (me.model.range_nm/rangeNm)*345; me.range_vor = me.element.createChild("path") @@ -37,7 +112,7 @@ var draw = func { .setStrokeDashArray([5, 15, 5, 15, 5]) .setColor(0,1,0); - var course = me.controller.get_tuned_course(me.model.frequency/100); + var course = me.layer.map.controller.get_tuned_course(me.model.frequency/100); me.radial_vor = me.element.createChild("path") .moveTo(0,-radius) .vert(2*radius) @@ -45,13 +120,14 @@ var draw = func { .setStrokeDashArray([15, 5, 15, 5, 15]) .setColor(0,1,0) .setRotation(course*D2R); - me.icon_vor.setColor(0,1,0); } me.range_vor.show(); me.radial_vor.show(); - } elsif (me.range_vor != nil) { - me.range_vor.hide(); - me.radial_vor.hide(); + } else { # inactive station (not tuned) + if (me.range_vor != nil) { + me.range_vor.hide(); + me.radial_vor.hide(); + } } }; diff --git a/Nasal/canvas/map/WPT.lcontroller b/Nasal/canvas/map/WPT.lcontroller index 1192d4cc5..f22c043d7 100644 --- a/Nasal/canvas/map/WPT.lcontroller +++ b/Nasal/canvas/map/WPT.lcontroller @@ -1,3 +1,4 @@ +# See: http://wiki.flightgear.org/MapStructure # Class things: var name = 'WPT'; # for waypoints var parents = [SymbolLayer.Controller]; @@ -12,8 +13,8 @@ var new = func(layer) { var m = { parents: [__self__], layer: layer, + map: layer.map, listeners: [], - query_range_nm: 25, }; return m; }; @@ -24,7 +25,7 @@ var del = func() { }; var searchCmd = func { - #print("Running query: "~name); + printlog(_MP_dbg_lvl, "Running query: "~name); var fp = flightplan(); var fpSize = fp.getPlanSize(); @@ -32,7 +33,6 @@ var searchCmd = func { for (var i = 1; i 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 = { + parents: [canvas.Map.Controller], + _pos: nil, _time: nil, query_range: func get_range(), is_tuned:is_tuned, get_tuned_course:get_course_by_freq, get_position: get_current_position, + new: func(map) return { parents:[controller], map:map }, + should_update_all: func { + # TODO: this is just copied from aircraftpos.controller, + # it really should be moved to somewhere common and reused + # and extended to fully differentiate between "static" + # and "volatile" layers. + var pos = me.map.getPosCoord(); + if (pos == nil) return 0; + var time = systime(); + if (me._pos == nil) + me._pos = geo.Coord.new(pos); + else { + var dist_m = me._pos.direct_distance_to(pos); + # 2 NM until we update again + if (dist_m < 2 * NM2M) return 0; + # Update at most every 4 seconds to avoid excessive stutter: + elsif (time - me._time < 4) return 0; + } + #print("update aircraft position"); + var (x,y,z) = pos.xyz(); + me._pos.set_xyz(x,y,z); + me._time = time; + return 1; + }, }; - - # FIXME: MapStructure: big hack - canvas.Symbol.Controller.get("VOR").query_range = controller.query_range; - canvas.Symbol.Controller.get("VOR").get_tuned_course = controller.get_tuned_course; - canvas.Symbol.Controller.get("DME").is_tuned = controller.is_tuned; - canvas.SymbolLayer.Controller.get("TFC").query_range = controller.query_range; - canvas.SymbolLayer.Controller.get("TFC").get_position = controller.get_position; + me.map.setController(controller); ### # set up various layers, controlled via callbacks in the controller hash @@ -938,11 +923,13 @@ var NavDisplay = { var render_target = (!contains(layer,'not_a_map') or !layer.not_a_map) ? me.map : me.nd; var the_layer = nil; - if(!layer['isMapStructure']) + if(!layer['isMapStructure']) # set up an old layer the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller ); else { printlog(_MP_dbg_lvl, "Setting up MapStructure-based layer for ND, name:", layer.name); - render_target.addLayer(factory: canvas.SymbolLayer, type_arg: layer.name); + var opt = options != nil and options[layer.name] != nil ? options[layer.name] :nil; + # print("Options is: ", opt!=nil?"enabled":"disabled"); + render_target.addLayer(factory: canvas.SymbolLayer, type_arg: layer.name, options:opt); the_layer = me.layers[layer.name] = render_target.getLayer(layer.name); } @@ -990,6 +977,14 @@ var NavDisplay = { # and update each model accordingly update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft { + var _time = systime(); + + # Variables: + var userLat = me.aircraft_source.get_lat(); + var userLon = me.aircraft_source.get_lon(); + var userGndSpd = me.aircraft_source.get_gnd_spd(); + var userVSpd = me.aircraft_source.get_vspd(); + var dispLCD = me.get_switch('toggle_display_type') == "LCD"; # Heading update var userHdgMag = me.aircraft_source.get_hdg_mag(); var userHdgTru = me.aircraft_source.get_hdg_tru(); @@ -1005,6 +1000,12 @@ var NavDisplay = { var userHdg=userHdgMag; var userTrk=userTrkMag; } + # this should only ever happen when testing the experimental AI/MP ND driver hash (not critical) + # or when an error occurs (critical) + if (!userHdg or !userTrk or !userLat or !userLon) { + print("aircraft source invalid, returning !"); + return; + } if (me.aircraft_source.get_gnd_spd() < 80) userTrk = userHdg; @@ -1019,19 +1020,41 @@ var NavDisplay = { userHdgTrkTru = userHdgTru; me.symbols.hdgTrk.setText("HDG"); } - - var userLat = me.aircraft_source.get_lat(); - var userLon = me.aircraft_source.get_lon(); - var userGndSpd = me.aircraft_source.get_gnd_spd(); - var userVSpd = me.aircraft_source.get_vspd(); - var dispLCD = me.get_switch('toggle_display_type') == "LCD"; - # this should only ever happen when testing the experimental AI/MP ND driver hash (not critical) - if (!userHdg or !userTrk or !userLat or !userLon) { - print("aircraft source invalid, returning !"); - return; + # First, update the display position of the map + var pos = { + lat: nil, lon: nil, + alt: nil, hdg: nil, + range: nil, + }; + pos.range = me.rangeNm(); # avoid this here, use a listener instead + # reposition the map, change heading & range: + if(me.in_mode('toggle_display_mode', ['PLAN'])) { + pos.hdg = 0; + if (getprop(me.efis_path ~ "/inputs/plan-wpt-index") >= 0) { + pos.lat = getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/latitude-deg"); + pos.lon = getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/longitude-deg"); + } else { + pos.lat = me.map.getLat(); + pos.lon = me.map.getLon(); + } + } else { + pos.hdg = userHdgTrkTru; + pos.lat = userLat; + pos.lon = userLon; + } + call(me.map.setPos, [pos.lat, pos.lon], me.map, pos); + + # MapStructure update! + if (me.map.controller.should_update_all()) { + me.map.update(); + } else { + # TODO: ugly list here + me.map.update(func(layer) (var n=layer.type) == "TFC" or n == "APS"); } + # Other symbol update + # TODO: should be refactored! if(me.in_mode('toggle_display_mode', ['PLAN'])) me.map.setTranslation(512,512); elsif(me.get_switch('toggle_centered')) @@ -1105,22 +1128,8 @@ var NavDisplay = { me.symbols.range.setText(sprintf("%3.0f",me.rangeNm()/2)); - # reposition the map, change heading & range: - if(me.in_mode('toggle_display_mode', ['PLAN'])) { - me.map._node.getNode("hdg",1).setDoubleValue(0); - if (getprop(me.efis_path ~ "/inputs/plan-wpt-index") >= 0) { - me.map._node.getNode("ref-lat",1).setDoubleValue(getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/latitude-deg")); - me.map._node.getNode("ref-lon",1).setDoubleValue(getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/longitude-deg")); - } - } else { - me.map._node.getNode("ref-lat",1).setDoubleValue(userLat); - me.map._node.getNode("ref-lon",1).setDoubleValue(userLon); - } - # The set range of the map does not correspond to what we see in-sim!! - me.map._node.getNode("range",1).setDoubleValue(me.rangeNm()); # avoid this here, use a listener instead - # Hide heading bug 10 secs after change - var vhdg_bug = getprop("autopilot/settings/heading-bug-deg"); + var vhdg_bug = getprop("autopilot/settings/heading-bug-deg") or 0; var hdg_bug_active = getprop("autopilot/settings/heading-bug-active"); if (hdg_bug_active == nil) hdg_bug_active = 1; @@ -1133,8 +1142,8 @@ var NavDisplay = { me.symbols.curHdgPtr.setRotation((userHdg-userTrk)*D2R); me.symbols.curHdgPtr2.setRotation((userHdg-userTrk)*D2R); } - else - { + else + { me.symbols.trkInd.setRotation((userTrk-userHdg)*D2R); me.symbols.trkInd2.setRotation((userTrk-userHdg)*D2R); me.symbols.curHdgPtr.setRotation(0); @@ -1149,7 +1158,6 @@ var NavDisplay = { me.symbols.selHdgLine2.setRotation(hdgBugRot); me.symbols.compass.setRotation(-userHdgTrk*D2R); me.symbols.compassApp.setRotation(-userHdgTrk*D2R); - me.map._node.getNode("hdg",1).setDoubleValue(userHdgTrkTru); } if(me.get_switch('toggle_centered')) { if (me.in_mode('toggle_display_mode', ['APP','VOR'])) @@ -1336,5 +1344,9 @@ var NavDisplay = { me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') and me.in_mode('toggle_display_mode', ['MAP'])); + # Okay, _how_ do we hook this up with FGPlot? + #print("ND update took "~(systime()-_time)~" seconds"); + setprop("/instrumentation/navdisplay["~ NavDisplay.id ~"]/update-ms", systime() - _time); + } }; diff --git a/Nasal/canvas/map/parking.draw b/Nasal/canvas/map/parking.draw index 1d79bd544..5e4edff59 100644 --- a/Nasal/canvas/map/parking.draw +++ b/Nasal/canvas/map/parking.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure var draw_parking = func(group, apt, lod) { var group = group.createChild("group", "apt-"~apt.id); foreach(var park; apt.parking()) { diff --git a/Nasal/canvas/map/parking.layer b/Nasal/canvas/map/parking.layer index 052448711..34f26e593 100644 --- a/Nasal/canvas/map/parking.layer +++ b/Nasal/canvas/map/parking.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure #TODO: use custom Model/DataProvider var ParkingLayer = {}; # make(Layer); ParkingLayer.new = func(group, name) { diff --git a/Nasal/canvas/map/route.draw b/Nasal/canvas/map/route.draw index 77a714bc8..695eea88e 100644 --- a/Nasal/canvas/map/route.draw +++ b/Nasal/canvas/map/route.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure ## # Draw a route with tracks and waypoints # diff --git a/Nasal/canvas/map/route.layer b/Nasal/canvas/map/route.layer index 5981abe82..a40eddf36 100644 --- a/Nasal/canvas/map/route.layer +++ b/Nasal/canvas/map/route.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure var RouteLayer = {}; RouteLayer.new = func(group,name) { diff --git a/Nasal/canvas/map/route.model b/Nasal/canvas/map/route.model index 27dfc3487..7ec4e6a22 100644 --- a/Nasal/canvas/map/route.model +++ b/Nasal/canvas/map/route.model @@ -1,3 +1,4 @@ +# WARNING: *.model files will be deprecated, see: http://wiki.flightgear.org/MapStructure var RouteModel = {route_monitor:nil}; RouteModel.new = func make(LayerModel, RouteModel); diff --git a/Nasal/canvas/map/runway-nd.draw b/Nasal/canvas/map/runway-nd.draw index e045bd705..2284daefc 100644 --- a/Nasal/canvas/map/runway-nd.draw +++ b/Nasal/canvas/map/runway-nd.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure var draw_rwy_nd = func (group, rwy, controller=nil, lod=nil) { # print("drawing runways-nd"); diff --git a/Nasal/canvas/map/runway-nd.layer b/Nasal/canvas/map/runway-nd.layer index e7c27cf0f..21176ce69 100644 --- a/Nasal/canvas/map/runway-nd.layer +++ b/Nasal/canvas/map/runway-nd.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure var RunwayNDLayer = {}; RunwayNDLayer.new = func(group, name) { var m=Layer.new(group, name, RunwayNDModel ); diff --git a/Nasal/canvas/map/runway-nd.model b/Nasal/canvas/map/runway-nd.model index ee7db60da..f1fb46e2d 100644 --- a/Nasal/canvas/map/runway-nd.model +++ b/Nasal/canvas/map/runway-nd.model @@ -1,3 +1,4 @@ +# WARNING: *.model files will be deprecated, see: http://wiki.flightgear.org/MapStructure var RunwayNDModel = {}; RunwayNDModel.new = func make( LayerModel, RunwayNDModel ); diff --git a/Nasal/canvas/map/runways.draw b/Nasal/canvas/map/runways.draw index ab4b1d504..fcbe87ae8 100644 --- a/Nasal/canvas/map/runways.draw +++ b/Nasal/canvas/map/runways.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure #TODO: split: draw_single_runway(pos) diff --git a/Nasal/canvas/map/runways.layer b/Nasal/canvas/map/runways.layer index 2845acaad..f31615eef 100644 --- a/Nasal/canvas/map/runways.layer +++ b/Nasal/canvas/map/runways.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure #TODO: use custom Model/DataProvider var RunwayLayer = {}; # make(Layer); RunwayLayer.new = func(group, name) { diff --git a/Nasal/canvas/map/storm.draw b/Nasal/canvas/map/storm.draw index 37f8589f2..ec4dc3bbf 100644 --- a/Nasal/canvas/map/storm.draw +++ b/Nasal/canvas/map/storm.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure ## # draw a single storm symbol # diff --git a/Nasal/canvas/map/storms.layer b/Nasal/canvas/map/storms.layer index 6504af495..54ea7b7d7 100644 --- a/Nasal/canvas/map/storms.layer +++ b/Nasal/canvas/map/storms.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure var StormLayer = {}; StormLayer.new = func(group,name, controller) { var m=Layer.new(group, name, StormModel); diff --git a/Nasal/canvas/map/storms.model b/Nasal/canvas/map/storms.model index 0bc6447b2..5b4c19ad6 100644 --- a/Nasal/canvas/map/storms.model +++ b/Nasal/canvas/map/storms.model @@ -1,3 +1,4 @@ +# WARNING: *.model files will be deprecated, see: http://wiki.flightgear.org/MapStructure var StormModel = {}; StormModel.new = func make( LayerModel, StormModel ); diff --git a/Nasal/canvas/map/taxiways.draw b/Nasal/canvas/map/taxiways.draw index 522627d3f..743e446fa 100644 --- a/Nasal/canvas/map/taxiways.draw +++ b/Nasal/canvas/map/taxiways.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure 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 diff --git a/Nasal/canvas/map/taxiways.layer b/Nasal/canvas/map/taxiways.layer index 4eb06d2d9..f26bb5455 100644 --- a/Nasal/canvas/map/taxiways.layer +++ b/Nasal/canvas/map/taxiways.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure #TODO: use custom Model/DataProvider var TaxiwayLayer = {}; # make(Layer); TaxiwayLayer.new = func(group, name) { diff --git a/Nasal/canvas/map/tcas_arrow_a500.draw b/Nasal/canvas/map/tcas_arrow_a500.draw index 23b05a0bf..122f979ea 100644 --- a/Nasal/canvas/map/tcas_arrow_a500.draw +++ b/Nasal/canvas/map/tcas_arrow_a500.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure var draw_tcas_arrow_above_500 = func(group, lod=0) { group.createChild("path") .moveTo(0,-17) diff --git a/Nasal/canvas/map/tcas_arrow_b500.draw b/Nasal/canvas/map/tcas_arrow_b500.draw index 0d07dea65..18174096a 100644 --- a/Nasal/canvas/map/tcas_arrow_b500.draw +++ b/Nasal/canvas/map/tcas_arrow_b500.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure var draw_tcas_arrow_below_500 = func(group) { group.createChild("path") diff --git a/Nasal/canvas/map/test.layer b/Nasal/canvas/map/test.layer index d2bd296b8..85e47dac7 100644 --- a/Nasal/canvas/map/test.layer +++ b/Nasal/canvas/map/test.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure #TODO: use custom Model/DataProvider var TestLayer = {}; # make(Layer); diff --git a/Nasal/canvas/map/tower.draw b/Nasal/canvas/map/tower.draw index 0cac8f54e..728d4651b 100644 --- a/Nasal/canvas/map/tower.draw +++ b/Nasal/canvas/map/tower.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure var draw_tower = func (group, apt,lod) { var group = group.createChild("group", "tower"); # TODO: move to map_elements.nas (tower, runway, parking etc) diff --git a/Nasal/canvas/map/tower.layer b/Nasal/canvas/map/tower.layer index 099b2af78..900f4d7f7 100644 --- a/Nasal/canvas/map/tower.layer +++ b/Nasal/canvas/map/tower.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure var TowerLayer = {}; TowerLayer.new = func(group, name) { var m=Layer.new(group, name, AirportModel ); #FIXME: AirportModel can be shared by Taxiways, Runways etc!! diff --git a/Nasal/canvas/map/traffic.draw b/Nasal/canvas/map/traffic.draw index 12602a41b..e90d78aab 100644 --- a/Nasal/canvas/map/traffic.draw +++ b/Nasal/canvas/map/traffic.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure var draw_traffic = func(group, traffic, lod=0) { var a = traffic; diff --git a/Nasal/canvas/map/traffic.layer b/Nasal/canvas/map/traffic.layer index 121d1e1cc..8185b0c80 100644 --- a/Nasal/canvas/map/traffic.layer +++ b/Nasal/canvas/map/traffic.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure var MPTrafficLayer = {}; MPTrafficLayer.new = func(group,name, controller=nil) { var m=Layer.new(group, name, MPTrafficModel); diff --git a/Nasal/canvas/map/traffic.model b/Nasal/canvas/map/traffic.model index ee9fe2df3..e5d2bf98f 100644 --- a/Nasal/canvas/map/traffic.model +++ b/Nasal/canvas/map/traffic.model @@ -1,3 +1,4 @@ +# WARNING: *.model files will be deprecated, see: http://wiki.flightgear.org/MapStructure var MPTrafficModel = {}; MPTrafficModel.new = func make(LayerModel, MPTrafficModel); diff --git a/Nasal/canvas/map/vor.draw b/Nasal/canvas/map/vor.draw index edde296f2..fcf26c518 100644 --- a/Nasal/canvas/map/vor.draw +++ b/Nasal/canvas/map/vor.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure var draw_vor = func (group, vor, controller=nil, lod = 0) { if (0) { diff --git a/Nasal/canvas/map/vor.layer b/Nasal/canvas/map/vor.layer index c7ebb32f6..19fb17c7b 100644 --- a/Nasal/canvas/map/vor.layer +++ b/Nasal/canvas/map/vor.layer @@ -1,3 +1,4 @@ +# WARNING: *.layer files will be deprecated, see: http://wiki.flightgear.org/MapStructure var VORLayer = {}; VORLayer.new = func(group,name, controller) { var m=Layer.new(group, name, VORModel ); diff --git a/Nasal/canvas/map/vor.model b/Nasal/canvas/map/vor.model index 37c0db71f..47d3ca31a 100644 --- a/Nasal/canvas/map/vor.model +++ b/Nasal/canvas/map/vor.model @@ -1,3 +1,4 @@ +# WARNING: *.model files will be deprecated, see: http://wiki.flightgear.org/MapStructure var VORModel = {}; VORModel.new = func make( LayerModel, VORModel ); diff --git a/Nasal/canvas/map/waypoint.draw b/Nasal/canvas/map/waypoint.draw index 52cd51d01..5916c1100 100644 --- a/Nasal/canvas/map/waypoint.draw +++ b/Nasal/canvas/map/waypoint.draw @@ -1,3 +1,4 @@ +# WARNING: *.draw files will be deprecated, see: http://wiki.flightgear.org/MapStructure ## # Draw a waypoint symbol and waypoint name (Gijs' 744 ND.nas code) diff --git a/Nasal/geo.nas b/Nasal/geo.nas index d83609868..a08d4e486 100644 --- a/Nasal/geo.nas +++ b/Nasal/geo.nas @@ -328,7 +328,8 @@ var viewer_position = func { # 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). +# caller's namespace). If searchCmd returns nil, nothing +# happens, i.e. the diff is cancelled. var PositionedSearch = { new: func(searchCmd, onAdded, onRemoved, obj=nil) { return { @@ -351,6 +352,8 @@ var PositionedSearch = { return ret; }, diff: func(old, new) { + if (new == nil) + return [old, [], []]; var removed = old~[]; #copyvec var added = new~[]; # Mark common elements from removed and added: @@ -369,6 +372,10 @@ var PositionedSearch = { # Optimized search using C code var old = me.result~[]; #copyvec me.result = call(searchCmd, nil, me.obj); + if (me.result == nil) + { me.result = old; return } + if (typeof(me.result) != 'vector') die("geo.PositionedSearch(): A searchCmd must return a vector of elements or nil !!"); # TODO: Maybe make this a hash instead to wrap a vector, so that we can implement basic type-checking - e.g. doing isa(PositionedSearchResult, me.result) would be kinda neat and could help troubleshooting + else positioned.diff( old, me.result, func call(me.onAdded, arg, me.obj),