7ca8482b07
In time for 3.0. The API is still not fully complete, and not fully cleaned up, but this is good enough for this release cycle (and it should offer benefit longer term, if not now -- hopefully performance as well). Many thanks to Hooray as well, who has helped prepare things while I could not, and often suggested ideas.
504 lines
16 KiB
Text
504 lines
16 KiB
Text
var dump_obj = func(m) {
|
|
var h = {};
|
|
foreach (var k; keys(m))
|
|
if (k != "parents")
|
|
h[k] = m[k];
|
|
debug.dump(h);
|
|
};
|
|
|
|
##
|
|
# must be either of:
|
|
# 1) draw* callback, 2) SVG filename, 3) Drawable class (with styling/LOD support)
|
|
var SymbolDrawable = {
|
|
new: func() {
|
|
},
|
|
};
|
|
|
|
## wrapper for each element
|
|
## i.e. keeps the canvas and texture map coordinates
|
|
var CachedElement = {
|
|
new: func(canvas_path, name, source, offset) {
|
|
var m = {parents:[CachedElement] };
|
|
m.canvas_src = canvas_path;
|
|
m.name = name;
|
|
m.source = source;
|
|
m.offset = offset;
|
|
return m;
|
|
}, # new()
|
|
render: func(group) {
|
|
# create a raster image child in the render target/group
|
|
return
|
|
group.createChild("image", me.name)
|
|
.setFile( me.canvas_src )
|
|
# TODO: fix .setSourceRect() to accept a single vector for coordinates ...
|
|
.setSourceRect(left:me.source[0],top:me.source[1],right:me.source[2],bottom:me.source[3] , normalized:0)
|
|
.setTranslation(me.offset); # FIXME: make sure this stays like this and isn't overridden
|
|
}, # render()
|
|
}; # of CachedElement
|
|
|
|
var SymbolCache = {
|
|
# We can draw symbols either with left/top, centered,
|
|
# or right/bottom alignment. Specify two in a vector
|
|
# to mix and match, e.g. left/centered would be
|
|
# [SymbolCache.DRAW_LEFT_TOP,SymbolCache.DRAW_CENTERED]
|
|
DRAW_LEFT_TOP: 0.0,
|
|
DRAW_CENTERED: 0.5,
|
|
DRAW_RIGHT_BOTTOM: 1.0,
|
|
new: func(dim...) {
|
|
var m = { parents:[SymbolCache] };
|
|
# to keep track of the next free caching spot (in px)
|
|
m.next_free = [0, 0];
|
|
# to store each type of symbol
|
|
m.dict = {};
|
|
if (size(dim) == 1 and typeof(dim[0]) == 'vector')
|
|
dim = dim[0];
|
|
# Two sizes: canvas and symbol
|
|
if (size(dim) == 2) {
|
|
var canvas_x = var canvas_y = dim[0];
|
|
var image_x = var image_y = dim[1];
|
|
# Two widths (canvas and symbol) and then height/width ratio
|
|
} else if (size(dim) == 3) {
|
|
var (canvas_x,image_x,ratio) = dim;
|
|
var canvas_y = canvas_x * ratio;
|
|
var image_y = image_x * ratio;
|
|
# Explicit canvas and symbol widths/heights
|
|
} else if (size(dim) == 4) {
|
|
var (canvas_x,canvas_y,image_x,image_y) = dim;
|
|
}
|
|
m.canvas_sz = [canvas_x, canvas_y];
|
|
m.image_sz = [image_x, image_y];
|
|
|
|
# allocate a canvas
|
|
m.canvas_texture = canvas.new( {
|
|
"name": "SymbolCache"~canvas_x~'x'~canvas_y,
|
|
"size": m.canvas_sz,
|
|
"view": m.canvas_sz,
|
|
"mipmapping": 1
|
|
});
|
|
|
|
# add a placement
|
|
m.canvas_texture.addPlacement( {"type": "ref"} );
|
|
|
|
return m;
|
|
},
|
|
add: func(name, callback, draw_mode=0) {
|
|
if (typeof(draw_mode) == 'scalar')
|
|
var draw_mode0 = var draw_mode1 = draw_mode;
|
|
else var (draw_mode0,draw_mode1) = draw_mode;
|
|
# get canvas texture that we use as cache
|
|
# get next free spot in texture (column/row)
|
|
# run the draw callback and render into a group
|
|
var gr = me.canvas_texture.createGroup();
|
|
gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0,
|
|
me.next_free[1] + me.image_sz[1]*draw_mode1);
|
|
#settimer(func debug.dump ( gr.getTransformedBounds() ), 0); # XXX: these are only updated when rendered
|
|
#debug.dump ( gr.getTransformedBounds() );
|
|
gr.update(); # apparently this doesn't result in sane output from .getTransformedBounds() either
|
|
#debug.dump ( gr.getTransformedBounds() );
|
|
# draw the symbol
|
|
callback(gr);
|
|
# get the bounding box, i.e. coordinates for texture map, or use the .setTranslation() params
|
|
var coords = me.next_free~me.next_free;
|
|
foreach (var i; [0,1])
|
|
coords[i+2] += me.image_sz[i];
|
|
# get the offset we used to position correctly in the bounds of the canvas
|
|
var offset = [me.image_sz[0]*draw_mode0, me.image_sz[1]*draw_mode1];
|
|
# store texture map coordinates in lookup map using the name as identifier
|
|
me.dict[name] = CachedElement.new(me.canvas_texture.getPath(), name, coords, offset );
|
|
# update next free position in cache (column/row)
|
|
me.next_free[0] += me.image_sz[0];
|
|
if (me.next_free[0] >= me.canvas_sz[0])
|
|
{ me.next_free[0] = 0; me.next_free[1] += me.image_sz[1] }
|
|
if (me.next_free[1] >= me.canvas_sz[1])
|
|
die("SymbolCache: ran out of space after adding '"~name~"'");
|
|
}, # add()
|
|
get: func(name) {
|
|
if(!contains(me.dict,name)) die("No SymbolCache entry for key:"~ name);
|
|
return me.dict[name];
|
|
}, # get()
|
|
};
|
|
|
|
var Symbol = {
|
|
# Static/singleton:
|
|
registry: {},
|
|
add: func(type, class)
|
|
me.registry[type] = class,
|
|
get: func(type)
|
|
if ((var class = me.registry[type]) == nil)
|
|
die("unknown type '"~type~"'");
|
|
else return class,
|
|
# Calls corresonding symbol constructor
|
|
# @param group #Canvas.Group to place this on.
|
|
new: func(type, group, arg...) {
|
|
var ret = call((var class = me.get(type)).new, [group]~arg, class);
|
|
ret.element.set("symbol-type", type);
|
|
return ret;
|
|
},
|
|
# Non-static:
|
|
df_controller: nil, # default controller
|
|
# Update the drawing of this object (position and others).
|
|
update: func()
|
|
die("update() not implemented for this symbol type!"),
|
|
draw: func(group, model, lod)
|
|
die("draw() not implemented for this symbol type!"),
|
|
del: func()
|
|
die("del() not implemented for this symbol type!"),
|
|
}; # of Symbol
|
|
|
|
Symbol.Controller = {
|
|
# Static/singleton:
|
|
registry: {},
|
|
add: func(type, class)
|
|
me.registry[type] = class,
|
|
get: func(type)
|
|
if ((var class = me.registry[type]) == nil)
|
|
die("unknown type '"~type~"'");
|
|
else return class,
|
|
# Calls corresonding symbol constructor
|
|
# @param model Model to place this on.
|
|
new: func(type, model, arg...)
|
|
return call((var class = me.get(type)).new, [model]~arg, class),
|
|
# Non-static:
|
|
# Update anything related to a particular model. Returns whether the object needs updating:
|
|
update: func(model) return 1,
|
|
# Initialize a controller to an object (or initialize the controller itself):
|
|
init: func(model) ,
|
|
# Delete an object from this controller (or delete the controller itself):
|
|
del: func(model) ,
|
|
# Return whether this symbol/object is visible:
|
|
isVisible: func(model) return 1,
|
|
# Get the position of this symbol/object:
|
|
getpos: func(model), # default provided below
|
|
}; # of Symbol.Controller
|
|
|
|
var getpos_fromghost = func(positioned_g)
|
|
return [positioned_g.lat, positioned_g.lon];
|
|
|
|
# Generic getpos: get lat/lon from any object:
|
|
# (geo.Coord and positioned ghost currently)
|
|
Symbol.Controller.getpos = func(obj) {
|
|
if (typeof(obj) == 'ghost')
|
|
if (ghosttype(obj) == 'positioned' or ghosttype(obj) == 'Navaid' or ghosttype(obj)=='Fix' or ghosttype(obj)=='flightplan-leg')
|
|
return getpos_fromghost(obj);
|
|
else
|
|
die("bad ghost of type '"~ghosttype(obj)~"'");
|
|
if (typeof(obj) == 'hash')
|
|
if (isa(obj, geo.Coord))
|
|
return obj.latlon();
|
|
if (contains(obj,'lat') and contains(obj,'lon'))
|
|
return [obj.lat, obj.lon];
|
|
|
|
debug.dump(obj);
|
|
die("no suitable getpos() found! Of type: "~typeof(obj));
|
|
};
|
|
|
|
|
|
var assert_m = func(hash, member)
|
|
if (!contains(hash, member))
|
|
die("required field not found: '"~member~"'");
|
|
var assert_ms = func(hash, members...)
|
|
foreach (var m; members)
|
|
if (m != nil) assert_m(hash, m);
|
|
|
|
|
|
var DotSym = {
|
|
parents: [Symbol],
|
|
element_id: nil,
|
|
# Static/singleton:
|
|
makeinstance: func(name, hash) {
|
|
if (!isa(hash, DotSym))
|
|
die("OOP error");
|
|
#assert_ms(hash,
|
|
# "element_type", # type of Canvas element
|
|
# #"element_id", # optional Canvas id
|
|
# #"init", # initialize routine
|
|
# "draw", # init/update routine
|
|
# #getpos", # get position from model in [x_units,y_units] (optional)
|
|
#);
|
|
return Symbol.add(name, hash);
|
|
},
|
|
# For the instances returned from makeinstance:
|
|
# @param group The Canvas group to add this to.
|
|
# @param model A correct object (e.g. positioned ghost) as
|
|
# expected by the .draw file that represents
|
|
# metadata like position, speed, etc.
|
|
# @param controller Optional controller "glue". Each method
|
|
# is called with the model as the only argument.
|
|
new: func(group, model, controller=nil) {
|
|
var m = {
|
|
parents: [me],
|
|
group: group,
|
|
model: model,
|
|
controller: controller == nil ? me.df_controller : controller,
|
|
element: group.createChild(
|
|
me.element_type, me.element_id
|
|
),
|
|
};
|
|
if (m.controller != nil) {
|
|
#print("Creating controller");
|
|
temp = m.controller.new(m.model,m);
|
|
if (temp != nil)
|
|
m.controller = temp;
|
|
#print("Initializing controller");
|
|
m.controller.init(model);
|
|
}
|
|
else die("default controller not found");
|
|
|
|
m.init();
|
|
return m;
|
|
},
|
|
del: func() {
|
|
#print("DotSym.del()");
|
|
me.deinit();
|
|
if (me.controller != nil)
|
|
me.controller.del(me.model);
|
|
call(func me.model.del(), nil, var err=[]); # try...
|
|
if (err[0] != "No such member: del") # ... and either catch or rethrow
|
|
die(err[0]);
|
|
me.element.del();
|
|
},
|
|
# Default wrappers:
|
|
init: func() me.draw(),
|
|
deinit: func(),
|
|
update: func() {
|
|
if (me.controller != nil) {
|
|
if (!me.controller.update(me.model)) return;
|
|
elsif (!me.controller.isVisible(me.model)) {
|
|
me.element.hide();
|
|
return;
|
|
}
|
|
} else
|
|
me.element.show();
|
|
me.draw();
|
|
var pos = me.controller.getpos(me.model);
|
|
if (size(pos) == 2)
|
|
pos~=[nil]; # fall through
|
|
if (size(pos) == 3)
|
|
var (lat,lon,rotation) = pos;
|
|
else die("bad position: "~debug.dump(pos));
|
|
me.element.setGeoPosition(lat,lon);
|
|
if (rotation != nil)
|
|
me.element.setRotation(rotation);
|
|
},
|
|
}; # of DotSym
|
|
|
|
# A layer that manages a list of symbols (using delta positioned handling).
|
|
var SymbolLayer = {
|
|
# Static/singleton:
|
|
registry: {},
|
|
add: func(type, class)
|
|
me.registry[type] = class,
|
|
get: func(type)
|
|
if ((var class = me.registry[type]) == nil)
|
|
die("unknown type '"~type~"'");
|
|
else return class,
|
|
# Non-static:
|
|
df_controller: nil, # default controller
|
|
df_priority: nil, # default priority for display sorting
|
|
type: nil, # type of #Symbol to add (MANDATORY)
|
|
id: nil, # id of the group #canvas.Element (OPTIONAL)
|
|
# @param group A group to place this on.
|
|
# @param controller A controller object (parents=[SymbolLayer.Controller])
|
|
# or implementation (parents[0].parents=[SymbolLayer.Controller]).
|
|
new: func(group, controller=nil) {
|
|
var m = {
|
|
parents: [me],
|
|
group: group.createChild("group", me.id), # TODO: the id is not properly set, but would be useful for debugging purposes (VOR, FIXES, NDB etc)
|
|
list: [],
|
|
};
|
|
# FIXME: hack to expose type of layer:
|
|
if (caller(1)[1] == Map.addLayer) {
|
|
var this_type = caller(1)[0].type_arg;
|
|
if (this_type != nil)
|
|
m.group.set("symbol-layer-type", this_type);
|
|
}
|
|
if (controller == nil)
|
|
#controller = SymbolLayer.Controller.new(me.type, m);
|
|
controller = me.df_controller;
|
|
assert_m(controller, "parents");
|
|
if (controller.parents[0] == SymbolLayer.Controller)
|
|
controller = controller.new(m);
|
|
assert_m(controller, "parents");
|
|
assert_m(controller.parents[0], "parents");
|
|
if (controller.parents[0].parents[0] != SymbolLayer.Controller)
|
|
die("OOP error");
|
|
m.controller = controller;
|
|
m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m);
|
|
m.update();
|
|
return m;
|
|
},
|
|
update: func() {
|
|
me.searcher.update();
|
|
foreach (var e; me.list)
|
|
e.update();
|
|
},
|
|
del: func() {
|
|
#print("SymbolLayer.del()");
|
|
me.controller.del();
|
|
foreach (var e; me.list)
|
|
e.del();
|
|
},
|
|
findsym: func(positioned_g, del=0) {
|
|
forindex (var i; me.list) {
|
|
var e = me.list[i];
|
|
if (geo.PositionedSearch._equals(e.model, positioned_g)) {
|
|
if (del) {
|
|
# Remove this element from the list
|
|
var prev = subvec(me.list, 0, i);
|
|
var next = subvec(me.list, i+1);
|
|
me.list = prev~next;
|
|
}
|
|
return e;
|
|
}
|
|
}
|
|
return nil;
|
|
},
|
|
searchCmd: func() me.controller.searchCmd(),
|
|
# Adds a symbol.
|
|
onAdded: func(positioned_g)
|
|
append(me.list, Symbol.new(me.type, me.group, positioned_g)),
|
|
# Removes a symbol
|
|
onRemoved: func(positioned_g)
|
|
me.findsym(positioned_g, 1).del(),
|
|
}; # of SymbolLayer
|
|
|
|
# Class to manage controlling a #SymbolLayer.
|
|
# Currently handles:
|
|
# * Searching for new symbols (positioned ghosts or other objects with unique id's).
|
|
# * Updating the layer (e.g. on an update loop or on a property change).
|
|
SymbolLayer.Controller = {
|
|
# Static/singleton:
|
|
registry: {},
|
|
add: func(type, class)
|
|
me.registry[type] = class,
|
|
get: func(type)
|
|
if ((var class = me.registry[type]) == nil)
|
|
die("unknown type '"~type~"'");
|
|
else return class,
|
|
# Calls corresonding controller constructor
|
|
# @param layer The #SymbolLayer this controller is responsible for.
|
|
new: func(type, layer, arg...)
|
|
return call((var class = me.get(type)).new, [layer]~arg, class),
|
|
# Non-static:
|
|
run_update: func() {
|
|
me.layer.update();
|
|
},
|
|
# @return List of positioned objects.
|
|
searchCmd: func()
|
|
die("searchCmd() not implemented for this SymbolLayer.Controller type!"),
|
|
}; # of SymbolLayer.Controller
|
|
|
|
var AnimatedLayer = {
|
|
};
|
|
|
|
var CompassLayer = {
|
|
};
|
|
|
|
var AltitudeArcLayer = {
|
|
};
|
|
|
|
load_MapStructure = func {
|
|
Map.Controller = {
|
|
# Static/singleton:
|
|
registry: {},
|
|
add: func(type, class)
|
|
me.registry[type] = class,
|
|
get: func(type)
|
|
if ((var class = me.registry[type]) == nil)
|
|
die("unknown type '"~type~"'");
|
|
else return class,
|
|
# Calls corresonding controller constructor
|
|
# @param map The #SymbolMap this controller is responsible for.
|
|
new: func(type, layer, arg...)
|
|
return call((var class = me.get(type)).new, [map]~arg, class),
|
|
};
|
|
|
|
####### LOAD FILES #######
|
|
#print("loading files");
|
|
(func {
|
|
var FG_ROOT = getprop("/sim/fg-root");
|
|
var load = func(file, name) {
|
|
#print(file);
|
|
if (name == nil)
|
|
var name = split("/", file)[-1];
|
|
if (substr(name, size(name)-4) == ".draw")
|
|
name = substr(name, 0, size(name)-5);
|
|
#print("reading file");
|
|
var code = io.readfile(file);
|
|
#print("compiling file");
|
|
# This segfaults for some reason:
|
|
#var code = call(compile, [code], var err=[]);
|
|
var code = call(func compile(code, file), [code], var err=[]);
|
|
if (size(err)) {
|
|
#print("handling error");
|
|
if (substr(err[0], 0, 12) == "Parse error:") { # hack around Nasal feature
|
|
var e = split(" at line ", err[0]);
|
|
if (size(e) == 2)
|
|
err[0] = string.join("", [e[0], "\n at ", file, ", line ", e[1], "\n "]);
|
|
}
|
|
for (var i = 1; (var c = caller(i)) != nil; i += 1)
|
|
err ~= subvec(c, 2, 2);
|
|
debug.printerror(err);
|
|
return;
|
|
}
|
|
#print("calling code");
|
|
call(code, nil, nil, var hash = {});
|
|
#debug.dump(keys(hash));
|
|
return hash;
|
|
};
|
|
|
|
var load_deps = func(name) {
|
|
load(FG_ROOT~"/Nasal/canvas/map/"~name~".lcontroller", name);
|
|
load(FG_ROOT~"/Nasal/canvas/map/"~name~".symbol", name);
|
|
load(FG_ROOT~"/Nasal/canvas/map/"~name~".scontroller", name);
|
|
}
|
|
|
|
foreach( var name; ['VOR','FIX','NDB','DME','WPT'] )
|
|
load_deps( name );
|
|
load(FG_ROOT~"/Nasal/canvas/map/aircraftpos.controller", name);
|
|
|
|
###
|
|
# set up a cache for 32x32 symbols
|
|
var SymbolCache32x32 = SymbolCache.new(1024,32);
|
|
|
|
var drawVOR = func(color, width=3) return func(group) {
|
|
# print("drawing vor");
|
|
var bbox = group.createChild("path")
|
|
.moveTo(-15,0)
|
|
.lineTo(-7.5,12.5)
|
|
.lineTo(7.5,12.5)
|
|
.lineTo(15,0)
|
|
.lineTo(7.5,-12.5)
|
|
.lineTo(-7.5,-12.5)
|
|
.close()
|
|
.setStrokeLineWidth(width)
|
|
.setColor( color );
|
|
# debug.dump( bbox.getBoundingBox() );
|
|
};
|
|
|
|
var cachedVOR1 = SymbolCache32x32.add( "VOR-BLUE", drawVOR( color:[0, 0.6, 0.85], width:3), SymbolCache.DRAW_CENTERED );
|
|
var cachedVOR2 = SymbolCache32x32.add( "VOR-RED" , drawVOR( color:[1.0, 0, 0], width: 3), SymbolCache.DRAW_CENTERED );
|
|
var cachedVOR3 = SymbolCache32x32.add( "VOR-GREEN" , drawVOR( color:[0, 1, 0], width: 3), SymbolCache.DRAW_CENTERED );
|
|
var cachedVOR4 = SymbolCache32x32.add( "VOR-WHITE" , drawVOR( color:[1, 1, 1], width: 3), SymbolCache.DRAW_CENTERED );
|
|
|
|
# STRESS TEST
|
|
if (0) {
|
|
for(var i=0;i <= 1024/32*4 - 4; i+=1)
|
|
SymbolCache32x32.add( "VOR-YELLOW"~i , drawVOR( color:[1, 1, 0], width: 3) );
|
|
|
|
var dlg = canvas.Window.new([640,320],"dialog");
|
|
var my_canvas = dlg.createCanvas().setColorBackground(1,1,1,1);
|
|
var root = my_canvas.createGroup();
|
|
|
|
SymbolCache32x32.get(name:"VOR-BLUE").render( group: root ).setGeoPosition(getprop("/position/latitude-deg"),getprop("/position/longitude-deg"));
|
|
}
|
|
|
|
})();
|
|
#print("finished loading files");
|
|
####### TEST SYMBOL #######
|
|
|
|
canvas.load_MapStructure = func;
|
|
|
|
}; # load_MapStructure
|
|
|
|
setlistener("/nasal/canvas/loaded", load_MapStructure); # end ugly module init listener hack
|