prepare for adopting MapStructure
[fg:hoorays-fgdata.git] / Nasal / canvas / MapStructure.nas
1 var dump_obj = func(m) {
2         var h = {};
3         foreach (var k; keys(m))
4                 if (k != "parents")
5                         h[k] = m[k];
6         debug.dump(h);
7 };
8
9 ##
10 # must be either of:
11 # 1) draw* callback, 2) SVG filename, 3) Drawable class (with styling/LOD support)
12 var SymbolDrawable = {
13         new: func() {
14         },
15 };
16
17 ## wrapper for each element
18 ## i.e. keeps the canvas and texture map coordinates
19 var CachedElement = {
20         new: func(canvas_path, name, source, offset) {
21                 var m = {parents:[CachedElement] };
22                 m.canvas_src = canvas_path;
23                 m.name = name;
24                 m.source = source;
25                 m.offset = offset;
26                 return m;
27         }, # new()
28         render: func(group) {
29                 # create a raster image child in the render target/group
30                 return
31                 group.createChild("image", me.name)
32                         .setFile( me.canvas_src )
33                         # TODO: fix .setSourceRect() to accept a single vector for coordinates ...
34                         .setSourceRect(left:me.source[0],top:me.source[1],right:me.source[2],bottom:me.source[3] , normalized:0)
35                         .setTranslation(me.offset); # FIXME: make sure this stays like this and isn't overridden
36         }, # render()
37 }; # of CachedElement
38
39 var SymbolCache = {
40         # We can draw symbols either with left/top, centered,
41         # or right/bottom alignment. Specify two in a vector
42         # to mix and match, e.g. left/centered would be
43         #  [SymbolCache.DRAW_LEFT_TOP,SymbolCache.DRAW_CENTERED]
44         DRAW_LEFT_TOP:     0.0,
45         DRAW_CENTERED:     0.5,
46         DRAW_RIGHT_BOTTOM: 1.0,
47         new: func(dim...) {
48                 var m = { parents:[SymbolCache] };
49                 # to keep track of the next free caching spot (in px)
50                 m.next_free = [0, 0];
51                 # to store each type of symbol
52                 m.dict = {};
53                 if (size(dim) == 1 and typeof(dim[0]) == 'vector')
54                         dim = dim[0];
55                 # Two sizes: canvas and symbol
56                 if (size(dim) == 2) {
57                         var canvas_x = var canvas_y = dim[0];
58                         var image_x = var image_y = dim[1];
59                 # Two widths (canvas and symbol) and then height/width ratio
60                 } else if (size(dim) == 3) {
61                         var (canvas_x,image_x,ratio) = dim;
62                         var canvas_y = canvas_x * ratio;
63                         var image_y = image_x * ratio;
64                 # Explicit canvas and symbol widths/heights
65                 } else if (size(dim) == 4) {
66                         var (canvas_x,canvas_y,image_x,image_y) = dim;
67                 }
68                 m.canvas_sz = [canvas_x, canvas_y];
69                 m.image_sz = [image_x, image_y];
70
71                 # allocate a canvas
72                 m.canvas_texture = canvas.new( {
73                         "name": "SymbolCache"~canvas_x~'x'~canvas_y,
74                         "size": m.canvas_sz,
75                         "view": m.canvas_sz,
76                         "mipmapping": 1
77                 });
78
79                 # add a placement
80                 m.canvas_texture.addPlacement( {"type": "ref"} );
81
82                 return m;
83         },
84         add: func(name, callback, draw_mode=0) {
85                 if (typeof(draw_mode) == 'scalar')
86                         var draw_mode0 = var draw_mode1 = draw_mode;
87                 else var (draw_mode0,draw_mode1) = draw_mode;
88                 # get canvas texture that we use as cache
89                 # get next free spot in texture (column/row)
90                 # run the draw callback and render into a group
91                 var gr = me.canvas_texture.createGroup();
92                 gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0,
93                                            me.next_free[1] + me.image_sz[1]*draw_mode1);
94                 settimer(func debug.dump ( gr.getTransformedBounds() ), 0); # XXX: these are only updated when rendered
95                 # draw the symbol
96                 callback(gr);
97                 # get the bounding box, i.e. coordinates for texture map, or use the .setTranslation() params
98                 var coords = me.next_free~me.next_free;
99                 foreach (var i; [0,1])
100                         coords[i+2] += me.image_sz[i];
101                 # get the offset we used to position correctly in the bounds of the canvas
102                 var offset = [me.image_sz[0]*draw_mode0, me.image_sz[1]*draw_mode1];
103                 # store texture map coordinates in lookup map using the name as identifier
104                 me.dict[name] = CachedElement.new(me.canvas_texture.getPath(), name, coords, offset );
105                 # update next free position in cache (column/row)
106                 me.next_free[0] += me.image_sz[0];
107                 if (me.next_free[0] >= me.canvas_sz[0])
108                 { me.next_free[0] = 0; me.next_free[1] += me.image_sz[1] }
109                 if (me.next_free[1] >= me.canvas_sz[1])
110                         die("SymbolCache: ran out of space after adding '"~name~"'");
111         }, # add()
112         get: func(name) {
113                 if(!contains(me.dict,name)) die("No SymbolCache entry for key:"~ name);
114                 return me.dict[name];
115         }, # get()
116 };
117
118 var Symbol = {
119 # Static/singleton:
120         registry: {},
121         add: func(type, class)
122                 me.registry[type] = class,
123         get: func(type)
124                 if ((var class = me.registry[type]) == nil)
125                         die("unknown type '"~type~"'");
126                 else return class,
127         # Calls corresonding symbol constructor
128         # @param group #Canvas.Group to place this on.
129         new: func(type, group, arg...) {
130                 var ret = call((var class = me.get(type)).new, [group]~arg, class);
131                 ret.element.set("symbol-type", type);
132                 return ret;
133         },
134 # Non-static:
135         df_controller: nil, # default controller
136         # Update the drawing of this object (position and others).
137         update: func()
138                 die("update() not implemented for this symbol type!"),
139         draw: func(group, model, lod)
140                 die("draw() not implemented for this symbol type!"),
141         del: func()
142                 die("del() not implemented for this symbol type!"),
143 }; # of Symbol
144
145 Symbol.Controller = {
146 # Static/singleton:
147         registry: {},
148         add: func(type, class)
149                 registry[type] = class,
150         get: func(type)
151                 if ((var class = me.registry[type]) == nil)
152                         die("unknown type '"~type~"'");
153                 else return class,
154         # Calls corresonding symbol constructor
155         # @param model Model to place this on.
156         new: func(type, model, arg...)
157                 return call((var class = me.get(type)).new, [model]~arg, class),
158 # Non-static:
159         # Update anything related to a particular model. Returns whether the object needs updating:
160         update: func(model) return 1,
161         # Initialize a controller to an object (or initialize the controller itself):
162         init: func(model) ,
163         # Delete an object from this controller (or delete the controller itself):
164         del: func(model) ,
165         # Return whether this symbol/object is visible:
166         isVisible: func(model) return 1,
167         # Get the position of this symbol/object:
168         getpos: func(model), # default provided below
169 }; # of Symbol.Controller
170
171 var getpos_fromghost = func(positioned_g)
172         return [positioned_g.lat, positioned_g.lon];
173
174 # Generic getpos: get lat/lon from any object:
175 # (geo.Coord and positioned ghost currently)
176 Symbol.Controller.getpos = func(obj) {
177         if (typeof(obj) == 'ghost')
178                 if (ghosttype(obj) == 'positioned' or ghosttype(obj) == 'Navaid' or ghosttype(obj)=='Fix' )
179                         return getpos_fromghost(obj);
180                 else
181                         die("bad ghost of type '"~ghosttype(obj)~"'");
182         if (typeof(obj) == 'hash')
183                 if (isa(obj, geo.Coord))
184                         return obj.latlon();
185         die("no suitable getpos() found! Of type: "~typeof(obj));
186 };
187
188
189 var assert_m = func(hash, member)
190         if (!contains(hash, member))
191                 die("required field not found: '"~member~"'");
192 var assert_ms = func(hash, members...)
193         foreach (var m; members)
194                 if (m != nil) assert_m(hash, m);
195
196
197 var DotSym = {
198         parents: [Symbol],
199         element_id: nil,
200 # Static/singleton:
201         makeinstance: func(name, hash) {
202                 if (!isa(hash, DotSym))
203                         die("OOP error");
204                 #assert_ms(hash,
205                 #       "element_type", # type of Canvas element
206                 #       #"element_id",  # optional Canvas id
207                 #       #"init",        # initialize routine
208                 #       "draw",         # init/update routine
209                 #       #getpos",       # get position from model in [x_units,y_units] (optional)
210                 #);
211                 return Symbol.add(name, hash);
212         },
213 # For the instances returned from makeinstance:
214         # @param group The Canvas group to add this to.
215         # @param model A correct object (e.g. positioned ghost) as
216         #              expected by the .draw file that represents
217         #              metadata like position, speed, etc.
218         # @param controller Optional controller "glue". Each method
219         #                   is called with the model as the only argument.
220         new: func(group, model, controller=nil) {
221                 var m = {
222                         parents: [me],
223                         group: group,
224                         model: model,
225                         controller: controller == nil ? me.df_controller : controller,
226                         element: group.createChild(
227                                 me.element_type, me.element_id
228                         ),
229                 };
230                 if (m.controller != nil) {
231                         #print("Initializing controller");
232                         m.controller.init(model);
233                 }
234                 else die("default controller not found");
235
236                 m.init();
237                 return m;
238         },
239         del: func() {
240                 #print("DotSym.del()");
241                 me.deinit();
242                 if (me.controller != nil)
243                         me.controller.del(me.model);
244                 call(func me.model.del(), nil, var err=[]); # try...
245                 if (err[0] != "No such member: del") # ... and either catch or rethrow
246                         die(err[0]);
247                 me.element.del();
248         },
249 # Default wrappers:
250         init: func() me.draw(),
251         deinit: func(),
252         update: func() {
253                 if (me.controller != nil) {
254                         if (!me.controller.update(me.model)) return;
255                         elsif (!me.controller.isVisible(me.model)) {
256                                 me.element.hide();
257                                 return;
258                         }
259                 } else
260                 me.element.show();
261                 me.draw();
262                 var pos = me.controller.getpos(me.model);
263                 if (size(pos) == 2)
264                         pos~=[nil]; # fall through
265                 if (size(pos) == 3)
266                         var (lat,lon,rotation) = pos;
267                 else die("bad position: "~debug.dump(pos));
268                 me.element.setGeoPosition(lat,lon);
269                 if (rotation != nil)
270                         me.element.setRotation(rotation);
271         },
272 }; # of DotSym
273
274 # A layer that manages a list of symbols (using delta positioned handling).
275 var SymbolLayer = {
276 # Static/singleton:
277         registry: {},
278         add: func(type, class)
279                 me.registry[type] = class,
280         get: func(type)
281                 if ((var class = me.registry[type]) == nil)
282                         die("unknown type '"~type~"'");
283                 else return class,
284 # Non-static:
285         df_controller: nil, # default controller
286         df_priority: nil, # default priority for display sorting
287         type: nil, # type of #Symbol to add (MANDATORY)
288         id: nil, # id of the group #canvas.Element (OPTIONAL)
289         # @param group A group to place this on.
290         # @param controller A controller object (parents=[SymbolLayer.Controller])
291         #                   or implementation (parents[0].parents=[SymbolLayer.Controller]).
292         new: func(group, controller=nil) {
293                 var m = {
294                         parents: [me],
295                         group: group.createChild("group", me.id), # TODO: the id is not properly set, but would be useful for debugging purposes (VOR, FIXES, NDB etc)
296                         list: [],
297                 };
298                 # FIXME: hack to expose type of layer:
299                 if (caller(1)[1] == Map.addLayer) {
300                         var this_type = caller(1)[0].type_arg;
301                         if (this_type != nil)
302                                 m.group.set("symbol-layer-type", this_type);
303                 }
304                 if (controller == nil)
305                         #controller = SymbolLayer.Controller.new(me.type, m);
306                         controller = me.df_controller;
307                 assert_m(controller, "parents");
308                 if (controller.parents[0] == SymbolLayer.Controller)
309                         controller = controller.new(m);
310                 assert_m(controller, "parents");
311                 assert_m(controller.parents[0], "parents");
312                 if (controller.parents[0].parents[0] != SymbolLayer.Controller)
313                         die("OOP error");
314                 m.controller = controller;
315                 m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m);
316                 m.update();
317                 return m;
318         },
319         update: func() {
320                 me.searcher.update();
321                 foreach (var e; me.list)
322                         e.update();
323         },
324         del: func() {
325                 #print("SymbolLayer.del()");
326                 me.controller.del();
327                 foreach (var e; me.list)
328                         e.del();
329         },
330         findsym: func(positioned_g, del=0) {
331                 forindex (var i; me.list) {
332                         var e = me.list[i];
333                         if (geo.PositionedSearch._equals(e.model, positioned_g)) {
334                                 if (del) {
335                                         # Remove this element from the list
336                                         var prev = subvec(me.list, 0, i);
337                                         var next = subvec(me.list, i+1);
338                                         me.list = prev~next;
339                                 }
340                                 return e;
341                         }
342                 }
343                 return nil;
344         },
345         searchCmd: func() me.controller.searchCmd(),
346         # Adds a symbol.
347         onAdded: func(positioned_g)
348                 append(me.list, Symbol.new(me.type, me.group, positioned_g)),
349         # Removes a symbol
350         onRemoved: func(positioned_g)
351                 me.findsym(positioned_g, 1).del(),
352 }; # of SymbolLayer
353
354 # Class to manage controlling a #SymbolLayer.
355 # Currently handles:
356 #  * Searching for new symbols (positioned ghosts or other objects with unique id's).
357 #  * Updating the layer (e.g. on an update loop or on a property change).
358 SymbolLayer.Controller = {
359 # Static/singleton:
360         registry: {},
361         add: func(type, class)
362                 me.registry[type] = class,
363         get: func(type)
364                 if ((var class = me.registry[type]) == nil)
365                         die("unknown type '"~type~"'");
366                 else return class,
367         # Calls corresonding controller constructor
368         # @param layer The #SymbolLayer this controller is responsible for.
369         new: func(type, layer, arg...)
370                 return call((var class = me.get(type)).new, [layer]~arg, class),
371 # Non-static:
372         run_update: func() {
373                 me.layer.update();
374         },
375         # @return List of positioned objects.
376         searchCmd: func()
377                 die("searchCmd() not implemented for this SymbolLayer.Controller type!"),
378 }; # of SymbolLayer.Controller
379
380 var AnimatedLayer = {
381 };
382
383 var CompassLayer = {
384 };
385
386 var AltitudeArcLayer = {
387 };
388
389 settimer(func {
390 Map.Controller = {
391 # Static/singleton:
392         registry: {},
393         add: func(type, class)
394                 me.registry[type] = class,
395         get: func(type)
396                 if ((var class = me.registry[type]) == nil)
397                         die("unknown type '"~type~"'");
398                 else return class,
399         # Calls corresonding controller constructor
400         # @param map The #SymbolMap this controller is responsible for.
401         new: func(type, layer, arg...)
402                 return call((var class = me.get(type)).new, [map]~arg, class),
403 };
404
405 ####### LOAD FILES #######
406 #print("loading files");
407 (func {
408         var FG_ROOT = getprop("/sim/fg-root");
409         var load = func(file, name) {
410                 #print(file);
411                 if (name == nil)
412                         var name = split("/", file)[-1];
413                 if (substr(name, size(name)-4) == ".draw")
414                         name = substr(name, 0, size(name)-5);
415                 #print("reading file");
416                 var code = io.readfile(file);
417                 #print("compiling file");
418                 # This segfaults for some reason:
419                 #var code = call(compile, [code], var err=[]);
420                 var code = call(func compile(code, file), [code], var err=[]);
421                 if (size(err)) {
422                         #print("handling error");
423                         if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature
424                                 var e = split(" at line ", err[0]);
425                                 if (size(e) == 2)
426                                         err[0] = string.join("", [e[0], "\n  at ", file, ", line ", e[1], "\n "]);
427                         }
428                         for (var i = 1; (var c = caller(i)) != nil; i += 1)
429                                 err ~= subvec(c, 2, 2);
430                         debug.printerror(err);
431                         return;
432                 }
433                 #print("calling code");
434                 call(code, nil, nil, var hash = {});
435                 #debug.dump(keys(hash));
436                 return hash;
437         };
438         load(FG_ROOT~"/Nasal/canvas/map/VOR.lcontroller", "VOR");
439         load(FG_ROOT~"/Nasal/canvas/map/VOR.symbol", "VOR");
440         load(FG_ROOT~"/Nasal/canvas/map/VOR.scontroller", "VOR");
441         load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", "VOR");
442
443         # FIXME: generalize and use a helper function for this stuff, i.e. DRY
444         load(FG_ROOT~"/Nasal/canvas/map/FIX.lcontroller", "FIX");
445         load(FG_ROOT~"/Nasal/canvas/map/FIX.symbol", "FIX"); # the current FIX.symbol file causes an interesting segfault in the Nasal engine/hash.c
446         load(FG_ROOT~"/Nasal/canvas/map/FIX.scontroller", "FIX");
447         load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", "FIX");
448
449 ###
450 # set up a cache for 32x32 symbols
451 var SymbolCache32x32 = SymbolCache.new(1024,32);
452
453 var drawVOR = func(color, width=3) return func(group) {
454                 # print("drawing vor");
455                 var bbox = group.createChild("path")
456                         .moveTo(-15,0)
457                         .lineTo(-7.5,12.5)
458                         .lineTo(7.5,12.5)
459                         .lineTo(15,0)
460                         .lineTo(7.5,-12.5)
461                         .lineTo(-7.5,-12.5)
462                         .close()
463                         .setStrokeLineWidth(width)
464                         .setColor( color );
465                 # debug.dump( bbox.getBoundingBox() );
466 };
467
468 var cachedVOR1 = SymbolCache32x32.add( "VOR-BLUE", drawVOR( color:[0, 0.6, 0.85], width:3), SymbolCache.DRAW_CENTERED ); 
469 var cachedVOR2 = SymbolCache32x32.add( "VOR-RED" , drawVOR( color:[1.0, 0, 0], width: 3),   SymbolCache.DRAW_CENTERED ); 
470 var cachedVOR3 = SymbolCache32x32.add( "VOR-GREEN" , drawVOR( color:[0, 1, 0], width: 3),   SymbolCache.DRAW_CENTERED ); 
471 var cachedVOR4 = SymbolCache32x32.add( "VOR-WHITE" , drawVOR( color:[1, 1, 1], width: 3),   SymbolCache.DRAW_CENTERED ); 
472
473 # STRESS TEST
474 if (0)
475 for(var i=0;i <= 1024/32*4 - 4; i+=1)
476         SymbolCache32x32.add( "VOR-YELLOW"~i , drawVOR( color:[1, 1, 0], width: 3)   ); 
477
478 var dlg = canvas.Window.new([640,320],"dialog");
479 var my_canvas = dlg.createCanvas().setColorBackground(1,1,1,1);
480 var root = my_canvas.createGroup();
481
482 SymbolCache32x32.get(name:"VOR-BLUE").render( group: root ).setGeoPosition(getprop("/position/latitude-deg"),getprop("/position/longitude-deg"));
483
484 })();
485 #print("finished loading files");
486 ####### TEST SYMBOL #######
487
488 }, 0); # end ugly module init timer hack
489