################################################################################ ## MapStructure mapping/charting framework for Nasal/Canvas, by Philosopher ## See: http://wiki.flightgear.org/MapStructure ############################################################################### ####### ## Dev Notes: ## ## - consider adding two types of SymbolLayers (sub-classes): Static (fixed positions, navaids/fixes) Dynamic (frequently updated, TFC/WXR, regardless of aircraft position) ## - FLT should be managed by aircraftpos.controller probably (interestign corner case actually) ## - consider adding an Overlay, i.e. for things like compass rose, lat/lon coordinate grid, but also tiled map data fetched on line ## - consider patching svg.nas to allow elements to be styled via the options hash by rewriting attributes, could even support animations that way ## - style handling/defaults should be moved to symbol files probably ## - consider pre-populating layer environments via bind() by providing APIs and fields for sane defaults: ## - parents ## - __self__ ## - del (managing all listeners and timers) ## - searchCmd -> filtering ## ## APIs to be wrapped for each layer: ## printlog(), die(), debug.bt(), benchmark() var _MP_dbg_lvl = "debug"; #var _MP_dbg_lvl = "alert"; var makedie = func(prefix) func(msg) globals.die(prefix~" "~msg); var __die = makedie("MapStructure"); ## # Try to call a method on an object with no arguments. Should # work for both ghosts and hashes; catches the error only when # the method doesn't exist -- errors raised during the call # are re-thrown. # var try_aux_method = func(obj, method_name) { var name = ""; call(compile("obj."~method_name~"()", name), nil, var err=[]); # try... #debug.dump(err); if (size(err)) # ... and either leave caght or rethrow if (err[1] != name) die(err[0]); } ## # Combine a specific hash with a default hash, e.g. for # options/df_options and style/df_style in a SymbolLayer. # var default_hash = func(opt, df) { if (opt != nil and typeof(opt)=='hash') { if (df != nil and opt != df and !isa(opt, df)) { if (contains(opt, "parents")) opt.parents ~= [df]; else opt.parents = [df]; } return opt; } else return df; } ## # to be used for prototyping, performance & stress testing (especially with multiple instance driven by AI traffic) # var MapStructure_selfTest = func() { var temp = {}; temp.dlg = canvas.Window.new([600,400],"dialog"); temp.canvas = temp.dlg.createCanvas().setColorBackground(1,1,1,0.5); temp.root = temp.canvas.createGroup(); var TestMap = temp.root.createChild("map"); TestMap.setController("Aircraft position"); TestMap.setRange(25); # TODO: implement zooming/panning via mouse/wheel here, for lack of buttons :-/ TestMap.setTranslation( temp.canvas.get("view[0]")/2, temp.canvas.get("view[1]")/2 ); var r = func(name,vis=1,zindex=nil) return caller(0)[0]; # TODO: we'll need some z-indexing here, right now it's just random # TODO: use foreach/keys to show all layers in this case by traversing SymbolLayer.registry direclty ? # maybe encode implicit z-indexing for each lcontroller ctor call ? - i.e. preferred above/below order ? foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR'),r('APS'), ] ) TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name, visible: type.vis, priority: type.zindex, ); foreach(var type; [ r('OSM'), r('OpenAIP'), r('STAMEN') ]) { TestMap.addLayer(factory: canvas.OverlayLayer, type_arg: type.name, visible: type.vis, priority: type.zindex, style: Styles.get(type.name), options: Options.get(type.name) ); } }; # MapStructure_selfTest ## # wrapper for each cached element: keeps the canvas and # texture map coordinates for the corresponding raster image. # var CachedElement = { 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; }, render: func(group, trans0=0, trans1=0) { # create a raster image child in the render target/group var n = group.createChild("image", me.name) .setFile(me.canvas_src) .setSourceRect(me.source, 0) .setSize(me.size) .setTranslation(trans0,trans1); n.createTransform().setTranslation(me.offset); return n; }, }; # of CachedElement var SymbolCache = { # We can draw symbols either with left/top, centered, # or right/bottom alignment. Specify two in a vector # to mix and match, e.g. left/centered would be # [SymbolCache.DRAW_LEFT_TOP,SymbolCache.DRAW_CENTERED] DRAW_LEFT_TOP: 0.0, DRAW_CENTERED: 0.5, DRAW_RIGHT_BOTTOM: 1.0, new: func(dim...) { var m = { parents:[SymbolCache] }; # to keep track of the next free caching spot (in px) m.next_free = [0, 0]; # to store each type of symbol m.dict = {}; if (size(dim) == 1 and typeof(dim[0]) == 'vector') dim = dim[0]; # Two sizes: canvas and symbol if (size(dim) == 2) { var canvas_x = var canvas_y = dim[0]; var image_x = var image_y = dim[1]; # Two widths (canvas and symbol) and then height/width ratio } else if (size(dim) == 3) { var (canvas_x,image_x,ratio) = dim; var canvas_y = canvas_x * ratio; var image_y = image_x * ratio; # Explicit canvas and symbol widths/heights } else if (size(dim) == 4) { var (canvas_x,canvas_y,image_x,image_y) = dim; } m.canvas_sz = [canvas_x, canvas_y]; m.image_sz = [image_x, image_y]; # allocate a canvas m.canvas_texture = canvas.new( { "name": "SymbolCache"~canvas_x~'x'~canvas_y, "size": m.canvas_sz, "view": m.canvas_sz, "mipmapping": 1 }); 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"); # TODO: register a reset/re-init listener to optionally purge/rebuild the cache ? return m; }, ## # Add a cached symbol based on a drawing callback. # @note this assumes that the object added by callback # fits into the dimensions provided to the constructor, # and any larger dimensionalities are liable to be cut off. # add: func(name, callback, draw_mode=0) { if (typeof(draw_mode) == 'scalar') var draw_mode0 = var draw_mode1 = draw_mode; else var (draw_mode0,draw_mode1) = draw_mode; # get canvas texture that we use as cache # get next free spot in texture (column/row) # run the draw callback and render into a new group var gr = me.root.createChild("group",name); # draw the symbol into the group callback(gr); gr.update(); # if we need sane output from getTransformedBounds() #debug.dump ( gr.getTransformedBounds() ); gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0, me.next_free[1] + me.image_sz[1]*draw_mode1); # get assumed the bounding box, i.e. coordinates for texture map var coords = me.next_free~me.next_free; foreach (var i; [0,1]) 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]; # 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~"'"); if (contains(me.dict, name)) print("MapStructure/SymbolCache Warning: Overwriting existing cache entry named:", 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) { return me.dict[name]; }, # get() }; # Excerpt from gen module var denied_symbols = [ "", "func", "if", "else", "var", "elsif", "foreach", "for", "forindex", "while", "nil", "return", "break", "continue", ]; var issym = func(string) { foreach (var d; denied_symbols) if (string == d) return 0; var sz = size(string); var s = string[0]; if ((s < `a` or s > `z`) and (s < `A` or s > `Z`) and (s != `_`)) return 0; for (var i=1; i `z`) and (s < `A` or s > `Z`) and (s < `0` or s > `9`)) return 0; return 1; }; var internsymbol = func(symbol) { #assert("argument not a symbol", issym, symbol); if (!issym(symbol)) die("argument not a symbol"); var get_interned = compile(""" keys({"~symbol~":})[0] """); return get_interned(); }; var tryintern = func(symbol) issym(symbol) ? internsymbol(symbol) : symbol; # End excerpt # Helpers for below var unescape = func(s) string.replace(s~"", "'", "\\'"); var hashdup = func(_,rkeys=nil) { var h={}; var k=rkeys!=nil?rkeys:members(_); foreach (var k;k) h[tryintern(k)]=member(_,k); h } var opt_member = func(h,k) { if (contains(h, k)) return h[k]; if (contains(h, "parents")) { var _=h.parents; for (var i=0;i(