1 ################################################################################
2 ## MapStructure mapping/charting framework for Nasal/Canvas, by Philosopher
3 ## See: http://wiki.flightgear.org/MapStructure
4 ###############################################################################
10 ## - consider adding two types of SymbolLayers (sub-classes): Static (fixed positions, navaids/fixes) Dynamic (frequently updated, TFC/WXR, regardless of aircraft position)
11 ## - FLT should be managed by aircraftpos.controller probably (interestign corner case actually)
12 ## - consider adding an Overlay, i.e. for things like compass rose, lat/lon coordinate grid, but also tiled map data fetched on line
13 ## - consider patching svg.nas to allow elements to be styled via the options hash by rewriting attributes, could even support animations that way
14 ## - style handling/defaults should be moved to symbol files probably
15 ## - consider pre-populating layer environments via bind() by providing APIs and fields for sane defaults:
18 ## - del (managing all listeners and timers)
19 ## - searchCmd -> filtering
21 ## APIs to be wrapped for each layer:
22 ## printlog(), die(), debug.bt(), benchmark()
24 var _MP_dbg_lvl = "info";
25 #var _MP_dbg_lvl = "alert";
27 var makedie = func(prefix) func(msg) globals.die(prefix~" "~msg);
29 var __die = makedie("MapStructure");
32 # Try to call a method on an object with no arguments. Should
33 # work for both ghosts and hashes; catches the error only when
34 # the method doesn't exist -- errors raised during the call
37 var try_aux_method = func(obj, method_name) {
38 var name = "<test%"~id(caller(0)[0])~">";
39 call(compile("obj."~method_name~"()", name), nil, var err=[]); # try...
41 if (size(err)) # ... and either leave caght or rethrow
47 # Combine a specific hash with a default hash, e.g. for
48 # options/df_options and style/df_style in a SymbolLayer.
50 var default_hash = func(opt, df) {
51 if (opt != nil and typeof(opt)=='hash') {
52 if (df != nil and opt != df and !isa(opt, df)) {
53 if (contains(opt, "parents"))
63 # to be used for prototyping, performance & stress testing (especially with multiple instance driven by AI traffic)
66 var MapStructure_selfTest = func() {
68 temp.dlg = canvas.Window.new([600,400],"dialog");
69 temp.canvas = temp.dlg.createCanvas().setColorBackground(1,1,1,0.5);
70 temp.root = temp.canvas.createGroup();
71 var TestMap = temp.root.createChild("map");
72 TestMap.setController("Aircraft position");
73 TestMap.setRange(25); # TODO: implement zooming/panning via mouse/wheel here, for lack of buttons :-/
74 TestMap.setTranslation(
75 temp.canvas.get("view[0]")/2,
76 temp.canvas.get("view[1]")/2
78 var r = func(name,vis=1,zindex=nil) return caller(0)[0];
79 # TODO: we'll need some z-indexing here, right now it's just random
80 # TODO: use foreach/keys to show all layers in this case by traversing SymbolLayer.registry direclty ?
81 # maybe encode implicit z-indexing for each lcontroller ctor call ? - i.e. preferred above/below order ?
82 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'), ] )
83 TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name,
84 visible: type.vis, priority: type.zindex,
86 }; # MapStructure_selfTest
90 # wrapper for each cached element: keeps the canvas and
91 # texture map coordinates for the corresponding raster image.
94 new: func(canvas_path, name, source, size, offset) {
95 var m = {parents:[CachedElement] };
96 if (isa(canvas_path, canvas.Canvas)) {
97 canvas_path = canvas_path.getPath();
99 m.canvas_src = canvas_path;
107 render: func(group, trans0=0, trans1=0) {
108 # create a raster image child in the render target/group
109 var n = group.createChild("image", me.name)
110 .setFile(me.canvas_src)
111 .setSourceRect(me.source, 0)
113 .setTranslation(trans0,trans1);
114 n.createTransform().setTranslation(me.offset);
117 }; # of CachedElement
120 # We can draw symbols either with left/top, centered,
121 # or right/bottom alignment. Specify two in a vector
122 # to mix and match, e.g. left/centered would be
123 # [SymbolCache.DRAW_LEFT_TOP,SymbolCache.DRAW_CENTERED]
126 DRAW_RIGHT_BOTTOM: 1.0,
128 var m = { parents:[SymbolCache] };
129 # to keep track of the next free caching spot (in px)
130 m.next_free = [0, 0];
131 # to store each type of symbol
133 if (size(dim) == 1 and typeof(dim[0]) == 'vector')
135 # Two sizes: canvas and symbol
136 if (size(dim) == 2) {
137 var canvas_x = var canvas_y = dim[0];
138 var image_x = var image_y = dim[1];
139 # Two widths (canvas and symbol) and then height/width ratio
140 } else if (size(dim) == 3) {
141 var (canvas_x,image_x,ratio) = dim;
142 var canvas_y = canvas_x * ratio;
143 var image_y = image_x * ratio;
144 # Explicit canvas and symbol widths/heights
145 } else if (size(dim) == 4) {
146 var (canvas_x,canvas_y,image_x,image_y) = dim;
148 m.canvas_sz = [canvas_x, canvas_y];
149 m.image_sz = [image_x, image_y];
152 m.canvas_texture = canvas.new( {
153 "name": "SymbolCache"~canvas_x~'x'~canvas_y,
158 m.canvas_texture.setColorBackground(0, 0, 0, 0); #rgba
160 m.canvas_texture.addPlacement( {"type": "ref"} );
162 m.path = m.canvas_texture.getPath();
163 m.root = m.canvas_texture.createGroup("entries");
165 # TODO: register a reset/re-init listener to optionally purge/rebuild the cache ?
170 # Add a cached symbol based on a drawing callback.
171 # @note this assumes that the object added by callback
172 # fits into the dimensions provided to the constructor,
173 # and any larger dimensionalities are liable to be cut off.
175 add: func(name, callback, draw_mode=0) {
176 if (typeof(draw_mode) == 'scalar')
177 var draw_mode0 = var draw_mode1 = draw_mode;
178 else var (draw_mode0,draw_mode1) = draw_mode;
180 # get canvas texture that we use as cache
181 # get next free spot in texture (column/row)
182 # run the draw callback and render into a new group
183 var gr = me.root.createChild("group",name);
184 # draw the symbol into the group
187 gr.update(); # if we need sane output from getTransformedBounds()
188 #debug.dump ( gr.getTransformedBounds() );
189 gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0,
190 me.next_free[1] + me.image_sz[1]*draw_mode1);
192 # get assumed the bounding box, i.e. coordinates for texture map
193 var coords = me.next_free~me.next_free;
194 foreach (var i; [0,1])
195 coords[i+1] += me.image_sz[i];
196 foreach (var i; [0,1])
197 coords[i*2+1] = me.canvas_sz[i] - coords[i*2+1];
198 # get the offset we used to position correctly in the bounds of the canvas
199 var offset = [-me.image_sz[0]*draw_mode0, -me.image_sz[1]*draw_mode1];
201 # update next free position in cache (column/row)
202 me.next_free[0] += me.image_sz[0];
203 if (me.next_free[0] >= me.canvas_sz[0])
204 { me.next_free[0] = 0; me.next_free[1] += me.image_sz[1] }
205 if (me.next_free[1] >= me.canvas_sz[1])
206 __die("SymbolCache: ran out of space after adding '"~name~"'");
208 if (contains(me.dict, name)) print("MapStructure/SymbolCache Warning: Overwriting existing cache entry named:", name);
210 # store texture map coordinates in lookup map using the name as identifier
211 return me.dict[name] = CachedElement.new(
212 canvas_path: me.path,
220 return me.dict[name];
224 # Excerpt from gen module
225 var denied_symbols = [
226 "", "func", "if", "else", "var",
227 "elsif", "foreach", "for",
228 "forindex", "while", "nil",
229 "return", "break", "continue",
231 var issym = func(string) {
232 foreach (var d; denied_symbols)
233 if (string == d) return 0;
234 var sz = size(string);
236 if ((s < `a` or s > `z`) and
237 (s < `A` or s > `Z`) and
238 (s != `_`)) return 0;
239 for (var i=1; i<sz; i+=1)
240 if (((s=string[i]) != `_`) and
241 (s < `a` or s > `z`) and
242 (s < `A` or s > `Z`) and
243 (s < `0` or s > `9`)) return 0;
246 var internsymbol = func(symbol) {
247 #assert("argument not a symbol", issym, symbol);
248 if (!issym(symbol)) die("argument not a symbol");
249 var get_interned = compile("""
250 keys({"~symbol~":})[0]
252 return get_interned();
254 var tryintern = func(symbol) issym(symbol) ? internsymbol(symbol) : symbol;
259 var unescape = func(s) string.replace(s~"", "'", "\\'");
260 var hashdup = func(_,rkeys=nil) {
261 var h={}; var k=rkeys!=nil?rkeys:members(_);
262 foreach (var k;k) h[tryintern(k)]=member(_,k); h
264 var member = func(h,k) {
265 if (contains(h, k)) return h[k];
266 if (contains(h, "parents")) {
268 for (var i=0;i<size(_);i+=1)
269 if (contains(_[i], k)) return _[i][k];
270 elsif (contains(_[i], "parents") and size(_[i].parents))
271 {_=h.parents~_[i+1:];i=0}
273 die("member not found: '"~unescape(k)~"'");
275 var _in = func(vec,k) { foreach (var _;vec) if(_==k)return 1; 0; }
276 var members = func(h,vec=nil) {
277 if (vec == nil) vec = [];
278 foreach (var k; keys(h))
280 foreach (var p; h[k])
286 var serialize = func(m,others=nil) {
291 else return "'" ~ unescape(m) ~ "'";
292 if (others == nil) others = {};
294 if (contains(others, i)) return "...";
299 if (ret != "[") ret ~= ",";
300 ret ~= serialize(l,others);
303 } else die("type not supported for style serialization: '"~t~"'");
306 # Drawing functions have the form:
307 # func(group) { group.createChild(...).set<Option>(<option>); ... }
308 # The style is passed as (essentially) their local namespace/variables,
309 # while the group is a regular argument.
310 var call_draw = func(draw, style, arg=nil, relevant_keys=nil) {
311 return call(draw, arg, nil, hashdup(style,relevant_keys));
314 # Serialize a style into a string.
315 var style_string = func(style, relevant_keys=nil) {
316 if (relevant_keys == nil) relevant_keys = members(style);
317 relevant_keys = sort(relevant_keys, cmp);
319 foreach (var k; relevant_keys) {
320 var m = member(style,k);
321 if (m == nil) continue;
330 # A class to mix styling and caching. Using the above helpers it
331 # serializes style hashes.
333 var StyleableCacheable = {
335 # Construct an object.
336 # @param name Prefix to use for entries in the cache
337 # @param draw_func Function for the cache that will draw the
338 # symbol onto a group using the style parameters.
339 # @param cache The #SymbolCache to use for these symbols.
340 # @param draw_mode See #SymbolCache
341 # @param relevant_keys A list of keys for the style used by the
342 # draw_func. Although it defaults to all
343 # available keys, it is highly recommended
344 # that it be specified.
346 new: func(name, draw_func, cache, draw_mode=0, relevant_keys=nil) {
348 parents: [StyleableCacheable],
350 _draw_func: draw_func,
352 _draw_mode: draw_mode,
353 relevant_keys: relevant_keys,
356 # Note: configuration like active/inactive needs
357 # to also use the passed style hash, unless it is
358 # chosen not to cache symbols that are, e.g., active.
359 request: func(style) {
360 var s = style_string(style, me.relevant_keys);
361 #debug.dump(style, s);
363 var c = me._cache.get(s1);
364 if (c != nil) return c;
365 return me.draw(style,s1);
367 render: func(element, style) {
368 var c = me.request(style);
371 draw: func(style,s1) {
372 var fn = func call_draw(me._draw_func, style, arg, me.relevant_keys);
373 me._cache.add(s1, fn, me._draw_mode);
378 # A base class for Symbols placed on a map.
380 # Note: for the derived objects, the element is stored as obj.element.
381 # This is also part of the object's parents vector, which allows
382 # callers to use obj.setVisible() et al. However, for code that
383 # manipulates the element's path (if it is a Canvas Path), it is best
384 # to use obj.element.addSegmentGeo() et al. for consistency.
389 add: func(type, class)
390 me.registry[type] = class,
392 if ((var class = me.registry[type]) == nil)
393 __die("Symbol.get():unknown type '"~type~"'");
395 # Calls corresonding symbol constructor
396 # @param group #Canvas.Group to place this on.
397 # @param layer The #SymbolLayer this is a child of.
398 new: func(type, group, layer, arg...) {
399 var ret = call((var class = me.get(type)).new, [group, layer]~arg, class);
400 ret.element.set("symbol-type", type);
403 # Private constructor:
405 m.style = m.layer.style;
406 m.options = m.layer.options;
407 if (m.controller != nil) {
408 temp = m.controller.new(m,m.model);
412 else __die("Symbol._new(): default controller not found");
415 df_controller: nil, # default controller -- Symbol.Controller by default, see below
416 # Update the drawing of this object (position and others).
418 __die("Abstract Symbol.update(): not implemented for this symbol type!"),
421 if (me.controller != nil)
422 me.controller.del(me, me.model);
423 try_aux_method(me.model, "del");
426 # Add a text element with styling
427 newText: func(text=nil, color=nil) {
428 var t = me.element.createChild("text")
429 .setDrawMode( canvas.Text.TEXT )
431 .setFont(me.layer.style.font)
432 .setFontSize(me.layer.style.font_size);
440 Symbol.Controller = {
443 add: func(type, class)
444 me.registry[type] = class,
446 if ((var class = me.registry[type]) == nil)
447 __die("Symbol.Controller.get(): unknown type '"~type~"'");
449 # Calls corresonding symbol controller constructor
450 # @param model Model to control this object (position and other attributes).
451 new: func(type, symbol, model, arg...)
452 return call((var class = me.get(type)).new, [symbol, model]~arg, class),
454 # Update anything related to a particular model. Returns whether the object needs updating:
455 update: func(symbol, model) return 1,
456 # Delete an object from this controller (or delete the controller itself):
457 del: func(symbol, model) ,
458 # Return whether this model/symbol is (should be) visible:
459 isVisible: func(model) return 1,
460 # Get the position of this symbol/object:
461 getpos: func(model) , # default provided below
462 }; # of Symbol.Controller
463 # Add this to Symbol as the default controller, but replace the Static .new() method with a blank
464 Symbol.df_controller = { parents:[Symbol.Controller], new: func nil };
466 var getpos_fromghost = func(positioned_g)
467 return [positioned_g.lat, positioned_g.lon];
469 # to add support for additional ghosts, just append them to the vector below, possibly at runtime:
470 var supported_ghosts = ['positioned','Navaid','Fix','flightplan-leg','FGAirport'];
471 var is_positioned_ghost = func(obj) {
472 var gt = ghosttype(obj);
473 foreach(var ghost; supported_ghosts) {
474 if (gt == ghost) return 1; # supported ghost was found
476 return 0; # not a known/supported ghost
479 var register_supported_ghost = func(name) append(supported_ghosts, name);
481 # Generic getpos: get lat/lon from any object:
482 # (geo.Coord and positioned ghost currently)
483 Symbol.Controller.getpos = func(obj, p=nil) {
484 if (obj == nil) __die("Symbol.Controller.getpos(): received nil");
486 var ret = Symbol.Controller.getpos(obj, obj);
487 if (ret != nil) return ret;
488 if (contains(obj, "parents")) {
489 foreach (var p; obj.parents) {
490 var ret = Symbol.Controller.getpos(obj, p);
491 if (ret != nil) return ret;
495 __die("Symbol.Controller.getpos(): no suitable getpos() found! Of type: "~typeof(obj));
497 if (typeof(p) == 'ghost')
498 if ( is_positioned_ghost(p) )
499 return getpos_fromghost(obj);
501 __die("Symbol.Controller.getpos(): bad/unsupported ghost of type '"~ghosttype(obj)~"' (see MapStructure.nas Symbol.Controller.getpos() to add new ghosts)");
502 if (typeof(p) == 'hash')
504 return subvec(obj.latlon(), 0, 2);
507 obj.getValue("position/latitude-deg") or obj.getValue("latitude-deg"),
508 obj.getValue("position/longitude-deg") or obj.getValue("longitude-deg")
510 if (contains(p,'lat') and contains(p,'lon'))
511 return [obj.lat, obj.lon];
516 Symbol.Controller.equals = func(l, r, p=nil) {
517 if (l == r) return 1;
519 var ret = Symbol.Controller.equals(l, r, l);
520 if (ret != nil) return ret;
521 if (contains(l, "parents")) {
522 foreach (var p; l.parents) {
523 var ret = Symbol.Controller.equals(l, r, p);
524 if (ret != nil) return ret;
528 __die("Symbol.Controller: no suitable equals() found! Of type: "~typeof(l));
530 if (typeof(p) == 'ghost')
531 if ( is_positioned_ghost(p) )
534 __die("Symbol.Controller: bad/unsupported ghost of type '"~ghosttype(l)~"' (see MapStructure.nas Symbol.Controller.getpos() to add new ghosts)");
535 if (typeof(p) == 'hash')
536 # Somewhat arbitrary convention:
537 # * l.equals(r) -- instance method, i.e. uses "me" and "arg[0]"
538 # * parent._equals(l,r) -- class method, i.e. uses "arg[0]" and "arg[1]"
539 if (contains(p, "equals"))
541 if (contains(p, "_equals"))
542 return p._equals(l,r);
544 return nil; # scio correctum est
548 var assert_m = func(hash, member)
549 if (!contains(hash, member))
550 __die("assert_m: required field not found: '"~member~"'");
551 var assert_ms = func(hash, members...)
552 foreach (var m; members)
553 if (m != nil) assert_m(hash, m);
556 # Implementation for a particular type of symbol (for the *.symbol files)
563 makeinstance: func(name, hash) {
564 if (!isa(hash, DotSym))
565 __die("DotSym: OOP error");
566 return Symbol.add(name, hash);
568 # For the instances returned from makeinstance:
569 # @param group The #Canvas.Group to add this to.
570 # @param layer The #SymbolLayer this is a child of.
571 # @param model A correct object (e.g. positioned ghost) as
572 # expected by the .draw file that represents
573 # metadata like position, speed, etc.
574 # @param controller Optional controller "glue". Each method
575 # is called with the model as the only argument.
576 new: func(group, layer, model, controller=nil) {
577 if (me == nil) __die();
584 controller: controller == nil ? me.df_controller : controller,
585 element: group.createChild(
586 me.element_type, me.element_id
589 append(m.parents, m.element);
596 printlog(_MP_dbg_lvl, "DotSym.del()");
598 call(Symbol.del, nil, me);
602 init: func() me.draw(),
605 if (me.controller != nil) {
606 if (!me.controller.update(me, me.model)) return;
607 elsif (!me.controller.isVisible(me.model)) {
614 var pos = me.controller.getpos(me.model);
616 pos~=[nil]; # fall through
618 var (lat,lon,rotation) = pos;
619 else __die("DotSym.update(): bad position: "~debug.dump(pos));
620 # print(me.model.id, ": Position lat/lon: ", lat, "/", lon);
621 me.element.setGeoPosition(lat,lon);
623 me.element.setRotation(rotation);
628 # Small wrapper for DotSym: parse a SVG on init().
632 element_type: "group",
636 canvas.parsesvg(me.element, me.svg_path);
638 if (var scale = me.layer.style['scale_factor'])
639 me.element.setScale(scale);
641 __die("cacheable not implemented yet!");
650 # wrapper for symbols based on raster images (i.e. PNGs)
651 # TODO: generalize this and port WXR.symbol accordingly
655 element_type: "group",
657 size: [32,32], scale: 1,
660 me.element.createChild("image", me.name)
661 .setFile(me.file_path)
665 __die("cacheable not implemented yet!");
680 makeinstance: func(name, hash) {
681 if (!isa(hash, LineSymbol))
682 __die("LineSymbol: OOP error");
683 return Symbol.add(name, hash);
685 # For the instances returned from makeinstance:
686 new: func(group, layer, model, controller=nil) {
687 if (me == nil) __die("Need me reference for LineSymbol.new()");
688 if (typeof(model) != 'vector') __die("LineSymbol.new(): need a vector of points");
694 controller: controller == nil ? me.df_controller : controller,
695 element: group.createChild(
696 "path", me.element_id
699 append(m.parents, m.element);
707 if (!me.needs_update) return;
708 printlog(_MP_dbg_lvl, "redrawing a LineSymbol "~me.layer.type);
712 var cmd = canvas.Path.VG_MOVE_TO;
713 foreach (var m; me.model) {
714 var (lat,lon) = me.controller.getpos(m);
715 append(coords,"N"~lat);
716 append(coords,"E"~lon);
717 append(cmds,cmd); cmd = canvas.Path.VG_LINE_TO;
719 me.element.setDataGeo(cmds, coords);
720 me.element.update(); # this doesn't help with flickering, it seems
723 printlog(_MP_dbg_lvl, "LineSymbol.del()");
725 call(Symbol.del, nil, me);
729 init: func() me.draw(),
732 if (me.controller != nil) {
733 if (!me.controller.update(me, me.model)) return;
734 elsif (!me.controller.isVisible(me.model)) {
745 # Base class for a SymbolLayer, e.g. MultiSymbolLayer or SingleSymbolLayer.
748 # Default implementations/values:
749 df_controller: nil, # default controller
750 df_priority: nil, # default priority for display sorting
753 type: nil, # type of #Symbol to add (MANDATORY)
754 id: nil, # id of the group #canvas.Element (OPTIONAL)
757 add: func(type, class)
758 me.registry[type] = class,
760 foreach(var invalid; var invalid_types = [nil,'vector','hash'])
761 if ( (var t=typeof(type)) == invalid) __die(" invalid SymbolLayer type (non-scalar) of type:"~t);
762 if ((var class = me.registry[type]) == nil)
763 __die("SymbolLayer.get(): unknown type '"~type~"'");
766 # Calls corresonding layer constructor
767 # @param group #Canvas.Group to place this on.
768 # @param map The #Canvas.Map this is a member of.
769 # @param controller A controller object.
770 # @param style An alternate style.
771 # @param options Extra options/configurations.
772 # @param visible Initially set it up as visible or invisible.
773 new: func(type, group, map, controller=nil, style=nil, options=nil, visible=1, arg...) {
774 # XXX: Extra named arguments are (obviously) not preserved well...
775 var ret = call((var class = me.get(type)).new, [group, map, controller, style, options, visible]~arg, class);
776 ret.group.set("layer-type", type);
779 # Private constructor:
780 _new: func(m, style, controller, options) {
781 # print("SymbolLayer setup options:", m.options!=nil);
782 m.style = default_hash(style, m.df_style);
783 m.options = default_hash(options, m.df_options);
785 if (controller == nil)
786 controller = m.df_controller;
787 assert_m(controller, "parents");
788 if (controller.parents[0] == SymbolLayer.Controller)
789 controller = controller.new(m);
790 assert_m(controller, "parents");
791 assert_m(controller.parents[0], "parents");
792 if (controller.parents[0].parents[0] != SymbolLayer.Controller)
793 __die("MultiSymbolLayer: OOP error");
794 m.controller = controller;
797 del: func() if (me.controller != nil) { me.controller.del(); me.controller = nil },
798 update: func() __die("Abstract SymbolLayer.update() not implemented for this Layer"),
801 # Class to manage controlling a #SymbolLayer.
803 # * Searching for new symbols (positioned ghosts or other objects with unique id's).
804 # * Updating the layer (e.g. on an update loop or on a property change).
805 SymbolLayer.Controller = {
808 add: func(type, class)
809 me.registry[type] = class,
811 if ((var class = me.registry[type]) == nil)
812 __die("unknown type '"~type~"'");
814 # Calls corresonding controller constructor
815 # @param layer The #SymbolLayer this controller is responsible for.
816 new: func(type, layer, arg...)
817 return call((var class = me.get(type)).new, [layer]~arg, class),
818 # Default implementations for derived classes:
819 # @return List of positioned objects.
821 __die("Abstract method searchCmd() not implemented for this SymbolLayer.Controller type!"),
822 addVisibilityListener: func() {
824 append(m.listeners, setlistener(
825 m.layer._node.getNode("visible"),
826 func m.layer.update(),
827 #compile("m.layer.update()", "<layer visibility on node "~m.layer._node.getNode("visible").getPath()~" for layer "~m.layer.type~">"),
831 # Default implementations for derived objects:
832 # For SingleSymbolLayer: retreive the model object
833 getModel: func me._model, # assume they store it here - otherwise they can override this
834 }; # of SymbolLayer.Controller
837 # A layer that manages a list of symbols (using delta positioned handling
838 # with a searchCmd to retreive placements).
840 var MultiSymbolLayer = {
841 parents: [SymbolLayer],
842 # Default implementations/values:
843 # @param group A group to place this on.
844 # @param map The #Canvas.Map this is a member of.
845 # @param controller A controller object (parents=[SymbolLayer.Controller])
846 # or implementation (parents[0].parents=[SymbolLayer.Controller]).
847 # @param style An alternate style.
848 # @param options Extra options/configurations.
849 # @param visible Initially set it up as visible or invisible.
850 new: func(group, map, controller=nil, style=nil, options=nil, visible=1) {
851 #print("Creating new SymbolLayer instance");
852 if (me == nil) __die("MultiSymbolLayer constructor needs to know its parent class");
856 group: group.createChild("group", me.type),
859 append(m.parents, m.group);
860 m.setVisible(visible);
861 # N.B.: this has to be here for the controller
862 m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m);
863 SymbolLayer._new(m, style, controller, options);
869 if (!me.getVisible())
871 #debug.warn("update traceback for "~me.type);
874 me.searcher.update();
875 foreach (var e; me.list)
879 if (me.options != nil and me.options['update_wrapper'] !=nil) {
880 me.options.update_wrapper( me, updater ); # call external wrapper (usually for profiling purposes)
886 printlog(_MP_dbg_lvl, "MultiSymbolLayer.del()");
887 foreach (var e; me.list)
889 call(SymbolLayer.del, nil, me);
891 delsym: func(model) {
892 forindex (var i; me.list) {
894 if (Symbol.Controller.equals(e.model, model)) {
895 # Remove this element from the list
896 # TODO: maybe C function for this? extend pop() to accept index?
897 var prev = subvec(me.list, 0, i);
898 var next = subvec(me.list, i+1);
907 if (me.map.getPosCoord() == nil or me.map.getRange() == nil) {
908 print("Map not yet initialized, returning empty result set!");
909 return []; # handle maps not yet fully initialized
911 var result = me.controller.searchCmd();
913 var type=typeof(result);
914 if(type != 'nil' and type != 'vector')
915 __die("MultiSymbolLayer: searchCmd() method MUST return a vector of valid positioned ghosts/Geo.Coord objects or nil! (was:"~type~")");
919 onAdded: func(model) {
920 printlog(_MP_dbg_lvl, "Adding symbol of type "~me.type);
921 if (model == nil) __die("MultiSymbolLayer: Model was nil for layer:"~debug.string(me.type)~ " Hint:check your equality check method!");
922 append(me.list, Symbol.new(me.type, me.group, me, model));
925 onRemoved: func(model) {
926 printlog(_MP_dbg_lvl, "Deleting symbol of type "~me.type);
927 if (!me.delsym(model)) __die("model not found");
928 try_aux_method(model, "del");
929 #call(func model.del(), nil, var err = []); # try...
930 #if (size(err) and err[0] != "No such member: del") # ... and either catch or rethrow
933 }; # of MultiSymbolLayer
936 # A layer that manages a list of statically-positioned navaid symbols (using delta positioned handling
937 # with a searchCmd to retrieve placements).
938 # This is not yet supposed to work properly, it's just there to help get rid of all the identical boilerplate code
939 # in lcontroller files, so needs some reviewing and customizing still
941 var NavaidSymbolLayer = {
942 parents: [MultiSymbolLayer],
943 # static generator/functor maker:
945 #print("Creating searchCmd() for NavaidSymbolLayer:", query);
947 printlog(_MP_dbg_lvl, "Running query:", query);
948 var range = me.map.getRange();
949 if (range == nil) return;
950 return positioned.findWithinRange(me.map.getPosCoord(), range, query);
953 }; # of NavaidSymbolLayer
958 ## TODO: wrappers for Horizontal vs. Vertical layers ?
961 var SingleSymbolLayer = {
962 parents: [SymbolLayer],
963 # Default implementations/values:
964 # @param group A group to place this on.
965 # @param map The #Canvas.Map this is a member of.
966 # @param controller A controller object (parents=[SymbolLayer.Controller])
967 # or implementation (parents[0].parents=[SymbolLayer.Controller]).
968 # @param style An alternate style.
969 # @param options Extra options/configurations.
970 # @param visible Initially set it up as visible or invisible.
971 new: func(group, map, controller=nil, style=nil, options=nil, visible=1) {
972 #print("Creating new SymbolLayer instance");
973 if (me == nil) __die("SingleSymbolLayer constructor needs to know its parent class");
977 group: group.createChild("group", me.type),
979 append(m.parents, m.group);
980 m.setVisible(visible);
981 SymbolLayer._new(m, style, controller, options);
983 m.symbol = Symbol.new(m.type, m.group, m, m.controller.getModel());
988 if (!me.getVisible())
992 if (typeof(me.symbol.model) == 'hash') try_aux_method(me.symbol.model, "update");
996 if (me.options != nil and me.options['update_wrapper'] != nil) {
997 me.options.update_wrapper( me, updater ); # call external wrapper (usually for profiling purposes)
1003 printlog(_MP_dbg_lvl, "SymbolLayer.del()");
1005 call(SymbolLayer.del, nil, me);
1007 }; # of SingleSymbolLayer
1010 # set up a cache for 32x32 symbols (initialized below in load_MapStructure)
1011 var SymbolCache32x32 = nil;
1013 var load_MapStructure = func {
1014 canvas.load_MapStructure = func; # disable any subsequent attempt to load
1019 add: func(type, class)
1020 me.registry[type] = class,
1022 if ((var class = me.registry[type]) == nil)
1023 __die("unknown type '"~type~"'");
1025 # Calls corresonding controller constructor
1026 # @param map The #SymbolMap this controller is responsible for.
1027 new: func(type, map, arg...) {
1028 var m = call((var class = me.get(type)).new, [map]~arg, class);
1029 if (!contains(m, "map"))
1031 elsif (m.map != map and !isa(m.map, map) and (
1032 m.get_position != Map.Controller.get_position
1033 or m.query_range != Map.Controller.query_range
1034 or m.in_range != Map.Controller.in_range))
1035 { __die("m must store the map handle as .map if it uses the default method(s)"); }
1037 # Default implementations:
1038 get_position: func() {
1039 debug.warn("get_position is deprecated");
1040 return me.map.getLatLon()~[me.map.getAlt()];
1042 query_range: func() {
1043 debug.warn("query_range is deprecated");
1044 return me.map.getRange() or 30;
1046 in_range: func(lat, lon, alt=0) {
1047 var range = me.map.getRange();
1048 if(range == nil) __die("in_range: Invalid query range!");
1049 # print("Query Range is:", range );
1050 if (lat == nil or lon == nil) __die("in_range: lat/lon invalid");
1051 var pos = geo.Coord.new();
1052 pos.set_latlon(lat, lon, alt or 0);
1053 var map_pos = me.map.getPosCoord();
1055 return 0; # should happen *ONLY* when map is uninitialized
1056 var distance_m = pos.distance_to( map_pos );
1057 var is_in_range = distance_m < range * NM2M;
1058 # print("Distance:",distance_m*M2NM," nm in range check result:", is_in_range);
1063 ####### LOAD FILES #######
1065 var FG_ROOT = getprop("/sim/fg-root");
1066 var load = func(file, name) {
1068 var name = split("/", file)[-1];
1069 var code = io.readfile(file);
1070 var code = call(func compile(code, file), [code], var err=[]);
1072 if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature
1073 var e = split(" at line ", err[0]);
1075 err[0] = string.join("", [e[0], "\n at ", file, ", line ", e[1], "\n "]);
1077 for (var i = 1; (var c = caller(i)) != nil; i += 1)
1078 err ~= subvec(c, 2, 2);
1079 debug.printerror(err);
1083 call(code, nil, nil, var hash = {});
1086 var url = ' http://wiki.flightgear.org/MapStructure#';
1087 # TODO: these rules should be extended for all main files lcontroller/scontroller and symbol
1089 { extension:'symbol', symbol:'update', type:'func', error:' update() must not be overridden:', id:300},
1090 # Sorry, this one doesn't work with the new LineSymbol
1091 # { extension:'symbol', symbol:'draw', type:'func', required:1, error:' symbol files need to export a draw() routine:', id:301},
1092 # Sorry, this one doesn't work with the new SingleSymbolLayer
1093 # { extension:'lcontroller', symbol:'searchCmd', type:'func', required:1, error:' lcontroller without searchCmd method:', id:100},
1097 var makeurl = func(scope, id) url ~ scope ~ ':' ~ id;
1098 var bailout = func(file, message, scope, id) __die(file~message~"\n"~makeurl(scope,id) );
1100 var current_ext = split('.', file)[-1];
1101 foreach(var check; checks) {
1102 # check if we have any rules matching the current file extension
1103 if (current_ext == check.extension) {
1104 # check for fields that must not be overridden
1105 if (check['error'] != nil and
1106 hash[check.symbol]!=nil and !check['required'] and
1107 typeof(hash[check.symbol])==check.type ) {
1108 bailout(file,check.error,check.extension,check.id);
1111 # check for required fields
1112 if (check['required'] != nil and
1113 hash[check.symbol]==nil and
1114 typeof( hash[check.symbol]) != check.type) {
1115 bailout(file,check.error,check.extension,check.id);
1123 # sets up a shared symbol cache, which will be used by all MapStructure maps and layers
1124 canvas.SymbolCache32x32 = SymbolCache.new(1024,32);
1126 # Find files and load them:
1127 var contents_dir = FG_ROOT~"/Nasal/canvas/map/";
1129 # With these extensions, in this order:
1136 foreach (var d; dep_names) deps[d] = [];
1137 foreach (var f; directory(contents_dir)) {
1138 var ext = size(var s=split(".", f)) > 1 ? s[-1] : nil;
1139 foreach (var d; dep_names) {
1146 foreach (var d; dep_names) {
1147 foreach (var f; deps[d]) {
1148 var name = split(".", f)[0];
1149 load(contents_dir~f, name);
1154 }; # load_MapStructure
1156 setlistener("/nasal/canvas/loaded", load_MapStructure); # end ugly module init listener hack. FIXME: do smart Nasal bootstrapping, quod est callidus!
1157 # 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