bc87c2d2c2109d7c7959719073798b21db6ddc1e
[fg:fgdata.git] / Nasal / canvas / MapStructure.nas
1 ################################################################################
2 ## MapStructure mapping/charting framework for Nasal/Canvas, by Philosopher
3 ## See: http://wiki.flightgear.org/MapStructure
4 ###############################################################################
5
6
7 #######
8 ## Dev Notes:
9 ##
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:
16 ##      - parents
17 ##      - __self__
18 ##      - del (managing all listeners and timers)
19 ##      - searchCmd -> filtering 
20 ##
21 ##  APIs to be wrapped for each layer:
22 ##  printlog(), die(), debug.bt(), benchmark()
23
24 var _MP_dbg_lvl = "info";
25 #var _MP_dbg_lvl = "alert";
26
27 var makedie = func(prefix) func(msg) globals.die(prefix~" "~msg);
28
29 var __die = makedie("MapStructure");
30
31 ##
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
35 # are re-thrown.
36 #
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...
40         #debug.dump(err);
41         if (size(err)) # ... and either leave caght or rethrow
42                 if (err[1] != name)
43                         die(err[0]);
44 }
45
46 ##
47 # Combine a specific hash with a default hash, e.g. for
48 # options/df_options and style/df_style in a SymbolLayer.
49 #
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"))
54                                 opt.parents ~= [df];
55                         else
56                                 opt.parents = [df];
57                 }
58                 return opt;
59         } else return df;
60 }
61
62 ##
63 # to be used for prototyping, performance & stress testing (especially with multiple instance driven by AI traffic)
64 #
65
66 var MapStructure_selfTest = func() {
67         var temp = {};
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
77         );
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,
85                 );
86 }; # MapStructure_selfTest
87
88
89 ##
90 # wrapper for each cached element: keeps the canvas and
91 # texture map coordinates for the corresponding raster image.
92 #
93 var CachedElement = {
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();
98                 }
99                 m.canvas_src = canvas_path;
100                 m.name = name;
101                 m.source = source;
102                 m.size = size;
103                 m.offset = offset;
104                 return m;
105         },
106
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)
112                         .setSize(me.size)
113                         .setTranslation(trans0,trans1);
114                 n.createTransform().setTranslation(me.offset);
115                 return n;
116         },
117 }; # of CachedElement
118
119 var SymbolCache = {
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]
124         DRAW_LEFT_TOP:     0.0,
125         DRAW_CENTERED:     0.5,
126         DRAW_RIGHT_BOTTOM: 1.0,
127         new: func(dim...) {
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
132                 m.dict = {};
133                 if (size(dim) == 1 and typeof(dim[0]) == 'vector')
134                         dim = dim[0];
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;
147                 }
148                 m.canvas_sz = [canvas_x, canvas_y];
149                 m.image_sz = [image_x, image_y];
150
151                 # allocate a canvas
152                 m.canvas_texture = canvas.new( {
153                         "name": "SymbolCache"~canvas_x~'x'~canvas_y,
154                         "size": m.canvas_sz,
155                         "view": m.canvas_sz,
156                         "mipmapping": 1
157                 });
158                 m.canvas_texture.setColorBackground(0, 0, 0, 0); #rgba
159                 # add a placement
160                 m.canvas_texture.addPlacement( {"type": "ref"} );
161
162                 m.path = m.canvas_texture.getPath();
163                 m.root = m.canvas_texture.createGroup("entries");
164
165                 # TODO: register a reset/re-init listener to optionally purge/rebuild the cache ?
166
167                 return m;
168         },
169         ##
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.
174         #
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;
179
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
185                 callback(gr);
186
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);
191
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];
200
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~"'");
207
208                 if (contains(me.dict, name)) print("MapStructure/SymbolCache Warning: Overwriting existing cache entry named:", name);
209
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,
213                         name: name,
214                         source: coords,
215                         size:me.image_sz,
216                         offset: offset,
217                 );
218         }, # add()
219         get: func(name) {
220                 return me.dict[name];
221         }, # get()
222 };
223
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",
230 ];
231 var issym = func(string) {
232         foreach (var d; denied_symbols)
233                 if (string == d) return 0;
234         var sz = size(string);
235         var s = string[0];
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;
244         return 1;
245 };
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]
251         """);
252         return get_interned();
253 };
254 var tryintern = func(symbol) issym(symbol) ? internsymbol(symbol) : symbol;
255
256 # End excerpt
257
258 # Helpers for below
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
263 }
264 var member = func(h,k) {
265         if (contains(h, k)) return h[k];
266         if (contains(h, "parents")) {
267                 var _=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}
272         }
273         die("member not found: '"~unescape(k)~"'");
274 }
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))
279                 if (k == "parents")
280                         foreach (var p; h[k])
281                                 members(p,vec);
282                 elsif (!_in(vec,k))
283                         append(vec, k);
284         return vec;
285 }
286 var serialize = func(m,others=nil) {
287         var t = typeof(m);
288         if (t == 'scalar')
289                 if (num(m) != nil)
290                         return m~"";
291                 else return "'" ~ unescape(m) ~ "'";
292         if (others == nil) others = {};
293         var i = id(m);
294         if (contains(others, i)) return "...";
295         others[i] = nil;
296         if (t == 'vector') {
297                 var ret = "[";
298                 foreach (var l; m) {
299                         if (ret != "[") ret ~= ",";
300                         ret ~= serialize(l,others);
301                 }
302                 return ret~"]";
303         } else die("type not supported for style serialization: '"~t~"'");
304 }
305
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));
312 }
313
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);
318         var str = "";
319         foreach (var k; relevant_keys) {
320                 var m = member(style,k);
321                 if (m == nil) continue;
322                 if (str) str ~= ";";
323                 str ~= k ~ ":";
324                 str ~= serialize(m);
325         }
326         return str;
327 }
328
329 ##
330 # A class to mix styling and caching. Using the above helpers it
331 # serializes style hashes.
332 #
333 var StyleableCacheable = {
334         ##
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.
345         #
346         new: func(name, draw_func, cache, draw_mode=0, relevant_keys=nil) {
347                 return {
348                         parents: [StyleableCacheable],
349                         _name: name,
350                         _draw_func: draw_func,
351                         _cache: cache,
352                         _draw_mode: draw_mode,
353                         relevant_keys: relevant_keys,
354                 };
355         },
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);
362                 var s1 = me._name~s;
363                 var c = me._cache.get(s1);
364                 if (c != nil) return c;
365                 return me.draw(style,s1);
366         },
367         render: func(element, style) {
368                 var c = me.request(style);
369                 c.render(element);
370         },
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);
374         },
375 };
376
377 ##
378 # A base class for Symbols placed on a map.
379 #
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.
385 #
386 var Symbol = {
387 # Static/singleton:
388         registry: {},
389         add: func(type, class)
390                 me.registry[type] = class,
391         get: func(type)
392                 if ((var class = me.registry[type]) == nil)
393                         __die("Symbol.get():unknown type '"~type~"'");
394                 else return class,
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);
401                 return ret;
402         },
403         # Private constructor:
404         _new: func(m) {
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);
409                         if (temp != nil)
410                                 m.controller = temp;
411                 }
412                 else __die("Symbol._new(): default controller not found");
413         },
414 # Non-static:
415         df_controller: nil, # default controller -- Symbol.Controller by default, see below
416         # Update the drawing of this object (position and others).
417         update: func()
418                 __die("Abstract Symbol.update(): not implemented for this symbol type!"),
419         draw: func() ,
420         del: func() {
421                 if (me.controller != nil)
422                         me.controller.del(me, me.model);
423                 try_aux_method(me.model, "del");
424         },
425
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 )
430                         .setText(text)
431                         .setFont(me.layer.style.font)
432                         .setFontSize(me.layer.style.font_size);
433                 if (color != nil)
434                         t.setColor(color);
435                 return t;
436         },
437 }; # of Symbol
438
439
440 Symbol.Controller = {
441 # Static/singleton:
442         registry: {},
443         add: func(type, class)
444                 me.registry[type] = class,
445         get: func(type)
446                 if ((var class = me.registry[type]) == nil)
447                         __die("Symbol.Controller.get(): unknown type '"~type~"'");
448                 else return class,
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),
453 # Non-static:
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 };
465
466 var getpos_fromghost = func(positioned_g)
467         return [positioned_g.lat, positioned_g.lon];
468
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
475         }
476         return 0; # not a known/supported ghost
477 };
478
479 var register_supported_ghost = func(name) append(supported_ghosts, name);
480
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");
485         if (p == 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;
492                         }
493                 }
494                 debug.dump(obj);
495                 __die("Symbol.Controller.getpos(): no suitable getpos() found! Of type: "~typeof(obj));
496         } else {
497                 if (typeof(p) == 'ghost')
498                         if ( is_positioned_ghost(p) )
499                                 return getpos_fromghost(obj);
500                         else
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')
503                         if (p == geo.Coord)
504                                 return subvec(obj.latlon(), 0, 2);
505                         if (p == props.Node)
506                                 return [
507                                         obj.getValue("position/latitude-deg")  or obj.getValue("latitude-deg"),
508                                         obj.getValue("position/longitude-deg") or obj.getValue("longitude-deg")
509                                 ];
510                         if (contains(p,'lat') and contains(p,'lon'))
511                                 return [obj.lat, obj.lon];
512                 return nil;
513         }
514 };
515
516 Symbol.Controller.equals = func(l, r, p=nil) {
517         if (l == r) return 1;
518         if (p == nil) {
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;
525                         }
526                 }
527                 debug.dump(l);
528                 __die("Symbol.Controller: no suitable equals() found! Of type: "~typeof(l));
529         } else {
530                 if (typeof(p) == 'ghost')
531                         if ( is_positioned_ghost(p) )
532                                 return l.id == r.id;
533                         else
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"))
540                                 return l.equals(r);
541                         if (contains(p, "_equals"))
542                                 return p._equals(l,r);
543         }
544         return nil; # scio correctum est
545 };
546
547
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);
554
555 ##
556 # Implementation for a particular type of symbol (for the *.symbol files)
557 # to handle details.
558 #
559 var DotSym = {
560         parents: [Symbol],
561         element_id: nil,
562 # Static/singleton:
563         makeinstance: func(name, hash) {
564                 if (!isa(hash, DotSym))
565                         __die("DotSym: OOP error");
566                 return Symbol.add(name, hash);
567         },
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();
578                 var m = {
579                         parents: [me],
580                         group: group,
581                         layer: layer,
582                         model: model,
583                         map: layer.map,
584                         controller: controller == nil ? me.df_controller : controller,
585                         element: group.createChild(
586                                 me.element_type, me.element_id
587                         ),
588                 };
589                 append(m.parents, m.element);
590                 Symbol._new(m);
591
592                 m.init();
593                 return m;
594         },
595         del: func() {
596                 printlog(_MP_dbg_lvl, "DotSym.del()");
597                 me.deinit();
598                 call(Symbol.del, nil, me);
599                 me.element.del();
600         },
601 # Default wrappers:
602         init: func() me.draw(),
603         deinit: func(),
604         update: func() {
605                 if (me.controller != nil) {
606                         if (!me.controller.update(me, me.model)) return;
607                         elsif (!me.controller.isVisible(me.model)) {
608                                 me.element.hide();
609                                 return;
610                         }
611                 } else
612                 me.element.show();
613                 me.draw();
614                 var pos = me.controller.getpos(me.model);
615                 if (size(pos) == 2)
616                         pos~=[nil]; # fall through
617                 if (size(pos) == 3)
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);
622                 if (rotation != nil)
623                         me.element.setRotation(rotation);
624         },
625 }; # of DotSym
626
627 ##
628 # Small wrapper for DotSym: parse a SVG on init().
629 #
630 var SVGSymbol = {
631         parents:[DotSym],
632         element_type: "group",
633         cacheable: 0,
634         init: func() {
635                 if (!me.cacheable) {
636                         canvas.parsesvg(me.element, me.svg_path);
637                         # hack:
638                         if (var scale = me.layer.style['scale_factor'])
639                                 me.element.setScale(scale);
640                 } else {
641                         __die("cacheable not implemented yet!");
642                 }
643                 me.draw();
644         },
645         draw: func,
646 }; # of SVGSymbol
647
648
649 ##
650 # wrapper for symbols based on raster images (i.e. PNGs)
651 # TODO: generalize this and port WXR.symbol accordingly
652 #
653 var RasterSymbol = {
654         parents:[DotSym],
655         element_type: "group",
656         cacheable: 0,
657         size: [32,32], scale: 1,
658         init: func() {
659                 if (!me.cacheable) {
660                         me.element.createChild("image", me.name)
661                         .setFile(me.file_path)
662                         .setSize(me.size)
663                         .setScale(me.scale);
664                 } else {
665                         __die("cacheable not implemented yet!");
666                 }
667                 me.draw();
668         },
669         draw: func,
670
671 }; # of RasterSymbol
672
673
674
675 var LineSymbol = {
676         parents:[Symbol],
677         element_id: nil,
678         needs_update: 1,
679 # Static/singleton:
680         makeinstance: func(name, hash) {
681                 if (!isa(hash, LineSymbol))
682                         __die("LineSymbol: OOP error");
683                 return Symbol.add(name, hash);
684         },
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");
689                 var m = {
690                         parents: [me],
691                         group: group,
692                         layer: layer,
693                         model: model,
694                         controller: controller == nil ? me.df_controller : controller,
695                         element: group.createChild(
696                                 "path", me.element_id
697                         ),
698                 };
699                 append(m.parents, m.element);
700                 Symbol._new(m);
701
702                 m.init();
703                 return m;
704         },
705 # Non-static:
706         draw: func() {
707                 if (!me.needs_update) return;
708                 printlog(_MP_dbg_lvl, "redrawing a LineSymbol "~me.layer.type);
709                 me.element.reset();
710                 var cmds = [];
711                 var coords = [];
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;
718                 }
719                 me.element.setDataGeo(cmds, coords);
720                 me.element.update(); # this doesn't help with flickering, it seems
721         },
722         del: func() {
723                 printlog(_MP_dbg_lvl, "LineSymbol.del()");
724                 me.deinit();
725                 call(Symbol.del, nil, me);
726                 me.element.del();
727         },
728 # Default wrappers:
729         init: func() me.draw(),
730         deinit: func(),
731         update: func() {
732                 if (me.controller != nil) {
733                         if (!me.controller.update(me, me.model)) return;
734                         elsif (!me.controller.isVisible(me.model)) {
735                                 me.element.hide();
736                                 return;
737                         }
738                 } else
739                 me.element.show();
740                 me.draw();
741         },
742 }; # of LineSymbol
743
744 ##
745 # Base class for a SymbolLayer, e.g. MultiSymbolLayer or SingleSymbolLayer.
746 #
747 var SymbolLayer = {
748 # Default implementations/values:
749         df_controller: nil, # default controller
750         df_priority: nil, # default priority for display sorting
751         df_style: nil,
752         df_options: nil,
753         type: nil, # type of #Symbol to add (MANDATORY)
754         id: nil, # id of the group #canvas.Element (OPTIONAL)
755 # Static/singleton:
756         registry: {},
757         add: func(type, class)
758                 me.registry[type] = class,
759         get: func(type) {
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~"'");
764                 else return class;
765         },
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);
777                 return ret;
778         },
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);
784                 
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;
795         },
796 # For instances:
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"),
799 };
800
801 # Class to manage controlling a #SymbolLayer.
802 # Currently handles:
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 = {
806 # Static/singleton:
807         registry: {},
808         add: func(type, class)
809                 me.registry[type] = class,
810         get: func(type)
811                 if ((var class = me.registry[type]) == nil)
812                         __die("unknown type '"~type~"'");
813                 else return class,
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.
820         searchCmd: func()
821                 __die("Abstract method searchCmd() not implemented for this SymbolLayer.Controller type!"),
822         addVisibilityListener: func() {
823                 var m = me;
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~">"),
828                         0,0
829                 ));
830         },
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
835
836 ##
837 # A layer that manages a list of symbols (using delta positioned handling
838 # with a searchCmd to retreive placements).
839 #
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");
853                 var m = {
854                         parents: [me],
855                         map: map,
856                         group: group.createChild("group", me.type),
857                         list: [],
858                 };
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);
864
865                 m.update();
866                 return m;
867         },
868         update: func() {
869                 if (!me.getVisible())
870                         return;
871                 #debug.warn("update traceback for "~me.type);
872
873                 var updater = func {
874                         me.searcher.update();
875                         foreach (var e; me.list)
876                                 e.update();
877                 }
878
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)
881                 } else {
882                         updater();
883                 }
884         },
885         del: func() {
886                 printlog(_MP_dbg_lvl, "MultiSymbolLayer.del()");
887                 foreach (var e; me.list)
888                         e.del();
889                 call(SymbolLayer.del, nil, me);
890         },
891         delsym: func(model) {
892                 forindex (var i; me.list) {
893                         var e = me.list[i];
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);
899                                 me.list = prev~next;
900                                 e.del();
901                                 return 1;
902                         }
903                 }
904                 return 0;
905         },
906         searchCmd: func() { 
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
910                 }
911                 var result = me.controller.searchCmd();
912                 # some hardening
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~")");
916                 return result;
917         },
918         # Adds a symbol.
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));
923         },
924         # Removes a symbol.
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
931                 #       die(err[0]);
932         },
933 }; # of MultiSymbolLayer
934
935 ##
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
940 #
941 var NavaidSymbolLayer = {
942         parents: [MultiSymbolLayer],
943 # static generator/functor maker:
944         make: func(query) {
945                 #print("Creating searchCmd() for NavaidSymbolLayer:", query);
946                 return func {
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);
951                 };
952         },
953 }; # of NavaidSymbolLayer
954
955
956
957 ###
958 ## TODO: wrappers for Horizontal vs. Vertical layers ?
959 ## 
960
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");
974                 var m = {
975                         parents: [me],
976                         map: map,
977                         group: group.createChild("group", me.type),
978                 };
979                 append(m.parents, m.group);
980                 m.setVisible(visible);
981                 SymbolLayer._new(m, style, controller, options);
982
983                 m.symbol = Symbol.new(m.type, m.group, m, m.controller.getModel());
984                 m.update();
985                 return m;
986         },
987         update: func() {
988                 if (!me.getVisible())
989                         return;
990
991                 var updater = func {
992                         if (typeof(me.symbol.model) == 'hash') try_aux_method(me.symbol.model, "update");
993                         me.symbol.update();
994                 }
995
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)
998                 } else {
999                         updater();
1000                 }
1001         },
1002         del: func() {
1003                 printlog(_MP_dbg_lvl, "SymbolLayer.del()");
1004                 me.symbol.del();
1005                 call(SymbolLayer.del, nil, me);
1006         },
1007 }; # of SingleSymbolLayer
1008
1009 ###
1010 # set up a cache for 32x32 symbols (initialized below in load_MapStructure)
1011 var SymbolCache32x32 = nil;
1012
1013 var load_MapStructure = func {
1014         canvas.load_MapStructure = func; # disable any subsequent attempt to load
1015
1016         Map.Controller = {
1017         # Static/singleton:
1018                 registry: {},
1019                 add: func(type, class)
1020                         me.registry[type] = class,
1021                 get: func(type)
1022                         if ((var class = me.registry[type]) == nil)
1023                                 __die("unknown type '"~type~"'");
1024                         else return class,
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"))
1030                                 m.map = 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)"); }
1036                 },
1037         # Default implementations:
1038                 get_position: func() {
1039                         debug.warn("get_position is deprecated");
1040                         return me.map.getLatLon()~[me.map.getAlt()];
1041                 },
1042                 query_range: func() {
1043                         debug.warn("query_range is deprecated");
1044                         return me.map.getRange() or 30;
1045                 },
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();
1054                         if (map_pos == nil)
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);
1059                         return is_in_range;
1060                 },
1061         };
1062
1063         ####### LOAD FILES #######
1064         (func {
1065                 var FG_ROOT = getprop("/sim/fg-root");
1066                 var load = func(file, name) {
1067                         if (name == nil)
1068                                 var name = split("/", file)[-1];
1069                         var code = io.readfile(file);
1070                         var code = call(func compile(code, file), [code], var err=[]);
1071                         if (size(err)) {
1072                                 if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature
1073                                         var e = split(" at line ", err[0]);
1074                                         if (size(e) == 2)
1075                                                 err[0] = string.join("", [e[0], "\n  at ", file, ", line ", e[1], "\n "]);
1076                                 }
1077                                 for (var i = 1; (var c = caller(i)) != nil; i += 1)
1078                                         err ~= subvec(c, 2, 2);
1079                                 debug.printerror(err);
1080                                 return;
1081                         }
1082                         #code=bind(
1083                         call(code, nil, nil, var hash = {});
1084
1085                         # validate
1086                         var url = ' http://wiki.flightgear.org/MapStructure#';
1087                         # TODO: these rules should be extended for all main files lcontroller/scontroller and symbol
1088                         var checks = [
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},
1094                                         ];
1095
1096
1097                         var makeurl = func(scope, id) url ~ scope ~ ':' ~ id;
1098                         var bailout = func(file, message, scope, id) __die(file~message~"\n"~makeurl(scope,id) );
1099
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);
1109                                         }
1110
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);
1116                                         }
1117                                 }
1118                         }
1119
1120                         return hash;
1121                 };
1122
1123                 # sets up a shared symbol cache, which will be used by all MapStructure maps and layers
1124                 canvas.SymbolCache32x32 = SymbolCache.new(1024,32);
1125
1126                 # Find files and load them:
1127                 var contents_dir = FG_ROOT~"/Nasal/canvas/map/";
1128                 var dep_names = [
1129                         # With these extensions, in this order:
1130                         "lcontroller",
1131                         "symbol",
1132                         "scontroller",
1133                         "controller",
1134                 ];
1135                 var deps = {};
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) {
1140                                 if (ext == d) {
1141                                         append(deps[d], f);
1142                                         break
1143                                 }
1144                         }
1145                 }
1146                 foreach (var d; dep_names) {
1147                         foreach (var f; deps[d]) {
1148                                 var name = split(".", f)[0];
1149                                 load(contents_dir~f, name);
1150                         }
1151                 }
1152         })();
1153
1154 }; # load_MapStructure
1155
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