################################################################################
## MapStructure mapping/charting framework for Nasal/Canvas, by Philosopher
## See: http://wiki.flightgear.org/MapStructure
###############################################################################


#######
## Dev Notes:
##
## - consider adding two types of SymbolLayers (sub-classes): Static (fixed positions, navaids/fixes) Dynamic (frequently updated, TFC/WXR, regardless of aircraft position)
## - FLT should be managed by aircraftpos.controller probably (interestign corner case actually)
## - consider adding an Overlay, i.e. for things like compass rose, lat/lon coordinate grid, but also tiled map data fetched on line
## - consider patching svg.nas to allow elements to be styled via the options hash by rewriting attributes, could even support animations that way
## - style handling/defaults should be moved to symbol files probably
## - consider pre-populating layer environments via bind() by providing APIs and fields for sane defaults:
##	- parents
##	- __self__
##	- del (managing all listeners and timers)
## 	- searchCmd -> filtering 
##
##  APIs to be wrapped for each layer:
##  printlog(), die(), debug.bt(), benchmark()

var _MP_dbg_lvl = "info";
#var _MP_dbg_lvl = "alert";

var makedie = func(prefix) func(msg) globals.die(prefix~" "~msg);

var __die = makedie("MapStructure");

##
# Try to call a method on an object with no arguments. Should
# work for both ghosts and hashes; catches the error only when
# the method doesn't exist -- errors raised during the call
# are re-thrown.
#
var try_aux_method = func(obj, method_name) {
	var name = "<test%"~id(caller(0)[0])~">";
	call(compile("obj."~method_name~"()", name), nil, var err=[]); # try...
	#debug.dump(err);
	if (size(err)) # ... and either leave caght or rethrow
		if (err[1] != name)
			die(err[0]);
}

##
# Combine a specific hash with a default hash, e.g. for
# options/df_options and style/df_style in a SymbolLayer.
#
var default_hash = func(opt, df) {
	if (opt != nil and typeof(opt)=='hash') {
		if (df != nil and opt != df and !isa(opt, df)) {
			if (contains(opt, "parents"))
				opt.parents ~= [df];
			else
				opt.parents = [df];
		}
		return opt;
	} else return df;
}

##
# to be used for prototyping, performance & stress testing (especially with multiple instance driven by AI traffic)
#

var MapStructure_selfTest = func() {
	var temp = {};
	temp.dlg = canvas.Window.new([600,400],"dialog");
	temp.canvas = temp.dlg.createCanvas().setColorBackground(1,1,1,0.5);
	temp.root = temp.canvas.createGroup();
	var TestMap = temp.root.createChild("map");
	TestMap.setController("Aircraft position");
	TestMap.setRange(25); # TODO: implement zooming/panning via mouse/wheel here, for lack of buttons :-/
	TestMap.setTranslation(
		temp.canvas.get("view[0]")/2,
		temp.canvas.get("view[1]")/2
	);
	var r = func(name,vis=1,zindex=nil) return caller(0)[0];
	# TODO: we'll need some z-indexing here, right now it's just random
	# TODO: use foreach/keys to show all layers in this case by traversing SymbolLayer.registry direclty ?
	# maybe encode implicit z-indexing for each lcontroller ctor call ? - i.e. preferred above/below order ?
	foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR'),r('APS'), ] ) 
		TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name,
					visible: type.vis, priority: type.zindex,
		);
}; # MapStructure_selfTest


##
# wrapper for each cached element: keeps the canvas and
# texture map coordinates for the corresponding raster image.
#
var CachedElement = {
	new: func(canvas_path, name, source, size, offset) {
		var m = {parents:[CachedElement] };
		if (isa(canvas_path, canvas.Canvas)) {
			canvas_path = canvas_path.getPath();
		}
		m.canvas_src = canvas_path;
		m.name = name;
		m.source = source;
		m.size = size;
		m.offset = offset;
		return m;
	},

	render: func(group, trans0=0, trans1=0) {
		# create a raster image child in the render target/group
		var n = group.createChild("image", me.name)
			.setFile(me.canvas_src)
			.setSourceRect(me.source, 0)
			.setSize(me.size)
			.setTranslation(trans0,trans1);
		n.createTransform().setTranslation(me.offset);
		return n;
	},
}; # of CachedElement

var SymbolCache = {
	# We can draw symbols either with left/top, centered,
	# or right/bottom alignment. Specify two in a vector
	# to mix and match, e.g. left/centered would be
	#  [SymbolCache.DRAW_LEFT_TOP,SymbolCache.DRAW_CENTERED]
	DRAW_LEFT_TOP:     0.0,
	DRAW_CENTERED:     0.5,
	DRAW_RIGHT_BOTTOM: 1.0,
	new: func(dim...) {
		var m = { parents:[SymbolCache] };
		# to keep track of the next free caching spot (in px)
		m.next_free = [0, 0];
		# to store each type of symbol
		m.dict = {};
		if (size(dim) == 1 and typeof(dim[0]) == 'vector')
			dim = dim[0];
		# Two sizes: canvas and symbol
		if (size(dim) == 2) {
			var canvas_x = var canvas_y = dim[0];
			var image_x = var image_y = dim[1];
		# Two widths (canvas and symbol) and then height/width ratio
		} else if (size(dim) == 3) {
			var (canvas_x,image_x,ratio) = dim;
			var canvas_y = canvas_x * ratio;
			var image_y = image_x * ratio;
		# Explicit canvas and symbol widths/heights
		} else if (size(dim) == 4) {
			var (canvas_x,canvas_y,image_x,image_y) = dim;
		}
		m.canvas_sz = [canvas_x, canvas_y];
		m.image_sz = [image_x, image_y];

		# allocate a canvas
		m.canvas_texture = canvas.new( {
			"name": "SymbolCache"~canvas_x~'x'~canvas_y,
			"size": m.canvas_sz,
			"view": m.canvas_sz,
			"mipmapping": 1
		});
		m.canvas_texture.setColorBackground(0, 0, 0, 0); #rgba
		# add a placement
		m.canvas_texture.addPlacement( {"type": "ref"} );

		m.path = m.canvas_texture.getPath();
		m.root = m.canvas_texture.createGroup("entries");

		# TODO: register a reset/re-init listener to optionally purge/rebuild the cache ?

		return m;
	},
	##
	# Add a cached symbol based on a drawing callback.
	# @note this assumes that the object added by callback
	#       fits into the dimensions provided to the constructor,
	#       and any larger dimensionalities are liable to be cut off.
	#
	add: func(name, callback, draw_mode=0) {
		if (typeof(draw_mode) == 'scalar')
			var draw_mode0 = var draw_mode1 = draw_mode;
		else var (draw_mode0,draw_mode1) = draw_mode;

		# get canvas texture that we use as cache
		# get next free spot in texture (column/row)
		# run the draw callback and render into a new group
		var gr = me.root.createChild("group",name);
		# draw the symbol into the group
		callback(gr);

		gr.update(); # if we need sane output from getTransformedBounds()
		#debug.dump ( gr.getTransformedBounds() );
		gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0,
		                   me.next_free[1] + me.image_sz[1]*draw_mode1);

		# get assumed the bounding box, i.e. coordinates for texture map
		var coords = me.next_free~me.next_free;
		foreach (var i; [0,1])
			coords[i+1] += me.image_sz[i];
		foreach (var i; [0,1])
			coords[i*2+1] = me.canvas_sz[i] - coords[i*2+1];
		# get the offset we used to position correctly in the bounds of the canvas
		var offset = [-me.image_sz[0]*draw_mode0, -me.image_sz[1]*draw_mode1];

		# update next free position in cache (column/row)
		me.next_free[0] += me.image_sz[0];
		if (me.next_free[0] >= me.canvas_sz[0])
		{ me.next_free[0] = 0; me.next_free[1] += me.image_sz[1] }
		if (me.next_free[1] >= me.canvas_sz[1])
			__die("SymbolCache: ran out of space after adding '"~name~"'");

		if (contains(me.dict, name)) print("MapStructure/SymbolCache Warning: Overwriting existing cache entry named:", name);

		# store texture map coordinates in lookup map using the name as identifier
		return me.dict[name] = CachedElement.new(
			canvas_path: me.path,
			name: name,
			source: coords,
			size:me.image_sz,
			offset: offset,
		);
	}, # add()
	get: func(name) {
		return me.dict[name];
	}, # get()
};

# Excerpt from gen module
var denied_symbols = [
	"", "func", "if", "else", "var",
	"elsif", "foreach", "for",
	"forindex", "while", "nil",
	"return", "break", "continue",
];
var issym = func(string) {
	foreach (var d; denied_symbols)
		if (string == d) return 0;
	var sz = size(string);
	var s = string[0];
	if ((s < `a` or s > `z`) and
		(s < `A` or s > `Z`) and
		(s != `_`)) return 0;
	for (var i=1; i<sz; i+=1)
		if (((s=string[i]) != `_`) and
			(s < `a` or s > `z`) and
			(s < `A` or s > `Z`) and
			(s < `0` or s > `9`)) return 0;
	return 1;
};
var internsymbol = func(symbol) {
	#assert("argument not a symbol", issym, symbol);
	if (!issym(symbol)) die("argument not a symbol");
	var get_interned = compile("""
		keys({"~symbol~":})[0]
	""");
	return get_interned();
};
var tryintern = func(symbol) issym(symbol) ? internsymbol(symbol) : symbol;

# End excerpt

# Helpers for below
var unescape = func(s) string.replace(s~"", "'", "\\'");
var hashdup = func(_,rkeys=nil) {
	var h={}; var k=rkeys!=nil?rkeys:members(_);
	foreach (var k;k) h[tryintern(k)]=member(_,k); h
}
var member = func(h,k) {
	if (contains(h, k)) return h[k];
	if (contains(h, "parents")) {
		var _=h.parents;
		for (var i=0;i<size(_);i+=1)
			if (contains(_[i], k)) return _[i][k];
			elsif (contains(_[i], "parents") and size(_[i].parents))
			{_=h.parents~_[i+1:];i=0}
	}
	die("member not found: '"~unescape(k)~"'");
}
var _in = func(vec,k) { foreach (var _;vec) if(_==k)return 1; 0; }
var members = func(h,vec=nil) {
	if (vec == nil) vec = [];
	foreach (var k; keys(h))
		if (k == "parents")
			foreach (var p; h[k])
				members(p,vec);
		elsif (!_in(vec,k))
			append(vec, k);
	return vec;
}
var serialize = func(m,others=nil) {
	var t = typeof(m);
	if (t == 'scalar')
		if (num(m) != nil)
			return m~"";
		else return "'" ~ unescape(m) ~ "'";
	if (others == nil) others = {};
	var i = id(m);
	if (contains(others, i)) return "...";
	others[i] = nil;
	if (t == 'vector') {
		var ret = "[";
		foreach (var l; m) {
			if (ret != "[") ret ~= ",";
			ret ~= serialize(l,others);
		}
		return ret~"]";
	} else die("type not supported for style serialization: '"~t~"'");
}

# Drawing functions have the form:
#   func(group) { group.createChild(...).set<Option>(<option>); ... }
# The style is passed as (essentially) their local namespace/variables,
# while the group is a regular argument.
var call_draw = func(draw, style, arg=nil, relevant_keys=nil) {
	return call(draw, arg, nil, hashdup(style,relevant_keys));
}

# Serialize a style into a string.
var style_string = func(style, relevant_keys=nil) {
	if (relevant_keys == nil) relevant_keys = members(style);
	relevant_keys = sort(relevant_keys, cmp);
	var str = "";
	foreach (var k; relevant_keys) {
		var m = member(style,k);
		if (m == nil) continue;
		if (str) str ~= ";";
		str ~= k ~ ":";
		str ~= serialize(m);
	}
	return str;
}

##
# A class to mix styling and caching. Using the above helpers it
# serializes style hashes.
#
var StyleableCacheable = {
	##
	# Construct an object.
	# @param name Prefix to use for entries in the cache
	# @param draw_func Function for the cache that will draw the
	#                  symbol onto a group using the style parameters.
	# @param cache The #SymbolCache to use for these symbols.
	# @param draw_mode See #SymbolCache
	# @param relevant_keys A list of keys for the style used by the
	#                      draw_func. Although it defaults to all
	#                      available keys, it is highly recommended
	#                      that it be specified.
	#
	new: func(name, draw_func, cache, draw_mode=0, relevant_keys=nil) {
		return {
			parents: [StyleableCacheable],
			_name: name,
			_draw_func: draw_func,
			_cache: cache,
			_draw_mode: draw_mode,
			relevant_keys: relevant_keys,
		};
	},
	# Note: configuration like active/inactive needs
	# to also use the passed style hash, unless it is
	# chosen not to cache symbols that are, e.g., active.
	request: func(style) {
		var s = style_string(style, me.relevant_keys);
		#debug.dump(style, s);
		var s1 = me._name~s;
		var c = me._cache.get(s1);
		if (c != nil) return c;
		return me.draw(style,s1);
	},
	render: func(element, style) {
		var c = me.request(style);
		c.render(element);
	},
	draw: func(style,s1) {
		var fn = func call_draw(me._draw_func, style, arg, me.relevant_keys);
		me._cache.add(s1, fn, me._draw_mode);
	},
};

##
# A base class for Symbols placed on a map.
#
# Note: for the derived objects, the element is stored as obj.element.
# This is also part of the object's parents vector, which allows
# callers to use obj.setVisible() et al. However, for code that
# manipulates the element's path (if it is a Canvas Path), it is best
# to use obj.element.addSegmentGeo() et al. for consistency.
#
var Symbol = {
# Static/singleton:
	registry: {},
	add: func(type, class)
		me.registry[type] = class,
	get: func(type)
		if ((var class = me.registry[type]) == nil)
			__die("Symbol.get():unknown type '"~type~"'");
		else return class,
	# Calls corresonding symbol constructor
	# @param group #Canvas.Group to place this on.
	# @param layer The #SymbolLayer this is a child of.
	new: func(type, group, layer, arg...) {
		var ret = call((var class = me.get(type)).new, [group, layer]~arg, class);
		ret.element.set("symbol-type", type);
		return ret;
	},
	# Private constructor:
	_new: func(m) {
		m.style = m.layer.style;
		m.options = m.layer.options;
		if (m.controller != nil) {
			temp = m.controller.new(m,m.model);
			if (temp != nil)
				m.controller = temp;
		}
		else __die("Symbol._new(): default controller not found");
	},
# Non-static:
	df_controller: nil, # default controller -- Symbol.Controller by default, see below
	# Update the drawing of this object (position and others).
	update: func()
		__die("Abstract Symbol.update(): not implemented for this symbol type!"),
	draw: func() ,
	del: func() {
		if (me.controller != nil)
			me.controller.del(me, me.model);
		try_aux_method(me.model, "del");
	},

	# Add a text element with styling
	newText: func(text=nil, color=nil) {
		var t = me.element.createChild("text")
			.setDrawMode( canvas.Text.TEXT )
			.setText(text)
			.setFont(me.layer.style.font)
			.setFontSize(me.layer.style.font_size);
		if (color != nil)
			t.setColor(color);
		return t;
	},
}; # 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("Symbol.Controller.get(): unknown type '"~type~"'");
		else return class,
	# Calls corresonding symbol controller constructor
	# @param model Model to control this object (position and other attributes).
	new: func(type, symbol, model, arg...)
		return call((var class = me.get(type)).new, [symbol, model]~arg, class),
# Non-static:
	# Update anything related to a particular model. Returns whether the object needs updating:
	update: func(symbol, model) return 1,
	# Delete an object from this controller (or delete the controller itself):
	del: func(symbol, model) ,
	# Return whether this model/symbol is (should be) visible:
	isVisible: func(model) return 1,
	# Get the position of this symbol/object:
	getpos: func(model) , # default provided below
}; # of Symbol.Controller
# Add this to Symbol as the default controller, but replace the Static .new() method with a blank
Symbol.df_controller = { parents:[Symbol.Controller], new: func nil };

var getpos_fromghost = func(positioned_g)
	return [positioned_g.lat, positioned_g.lon];

# to add support for additional ghosts, just append them to the vector below, possibly at runtime:
var supported_ghosts = ['positioned','Navaid','Fix','flightplan-leg','FGAirport'];
var is_positioned_ghost = func(obj) {
	var gt = ghosttype(obj);
	foreach(var ghost; supported_ghosts) {
		if (gt == ghost) return 1; # supported ghost was found
	}
	return 0; # not a known/supported ghost
};

var register_supported_ghost = func(name) append(supported_ghosts, name);

# Generic getpos: get lat/lon from any object:
# (geo.Coord and positioned ghost currently)
Symbol.Controller.getpos = func(obj, p=nil) {
	if (obj == nil) __die("Symbol.Controller.getpos(): received nil");
	if (p == nil) {
		var ret = Symbol.Controller.getpos(obj, obj);
		if (ret != nil) return ret;
		if (contains(obj, "parents")) {
			foreach (var p; obj.parents) {
				var ret = Symbol.Controller.getpos(obj, p);
				if (ret != nil) return ret;
			}
		}
		debug.dump(obj);
		__die("Symbol.Controller.getpos(): no suitable getpos() found! Of type: "~typeof(obj));
	} else {
		if (typeof(p) == 'ghost')
			if ( is_positioned_ghost(p) )
				return getpos_fromghost(obj);
			else
				__die("Symbol.Controller.getpos(): bad/unsupported ghost of type '"~ghosttype(obj)~"' (see MapStructure.nas Symbol.Controller.getpos() to add new ghosts)");
		if (typeof(p) == 'hash')
			if (p == geo.Coord)
				return subvec(obj.latlon(), 0, 2);
			if (p == props.Node)
				return [
					obj.getValue("position/latitude-deg")  or obj.getValue("latitude-deg"),
					obj.getValue("position/longitude-deg") or obj.getValue("longitude-deg")
				];
			if (contains(p,'lat') and contains(p,'lon'))
				return [obj.lat, obj.lon];
		return nil;
	}
};

Symbol.Controller.equals = func(l, r, p=nil) {
	if (l == r) return 1;
	if (p == nil) {
		var ret = Symbol.Controller.equals(l, r, l);
		if (ret != nil) return ret;
		if (contains(l, "parents")) {
			foreach (var p; l.parents) {
				var ret = Symbol.Controller.equals(l, r, p);
				if (ret != nil) return ret;
			}
		}
		debug.dump(l);
		__die("Symbol.Controller: no suitable equals() found! Of type: "~typeof(l));
	} else {
		if (typeof(p) == 'ghost')
			if ( is_positioned_ghost(p) )
				return l.id == r.id;
			else
				__die("Symbol.Controller: bad/unsupported ghost of type '"~ghosttype(l)~"' (see MapStructure.nas Symbol.Controller.getpos() to add new ghosts)");
		if (typeof(p) == 'hash')
			# Somewhat arbitrary convention:
			#   * l.equals(r)         -- instance method, i.e. uses "me" and "arg[0]"
			#   * parent._equals(l,r) -- class method, i.e. uses "arg[0]" and "arg[1]"
			if (contains(p, "equals"))
				return l.equals(r);
			if (contains(p, "_equals"))
				return p._equals(l,r);
	}
	return nil; # scio correctum est
};


var assert_m = func(hash, member)
	if (!contains(hash, member))
		__die("assert_m: required field not found: '"~member~"'");
var assert_ms = func(hash, members...)
	foreach (var m; members)
		if (m != nil) assert_m(hash, m);

##
# Implementation for a particular type of symbol (for the *.symbol files)
# to handle details.
#
var DotSym = {
	parents: [Symbol],
	element_id: nil,
# Static/singleton:
	makeinstance: func(name, hash) {
		if (!isa(hash, DotSym))
			__die("DotSym: OOP error");
		return Symbol.add(name, hash);
	},
# For the instances returned from makeinstance:
	# @param group The #Canvas.Group to add this to.
	# @param layer The #SymbolLayer this is a child of.
	# @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, layer, model, controller=nil) {
		if (me == nil) __die();
		var m = {
			parents: [me],
			group: group,
			layer: layer,
			model: model,
			map: layer.map,
			controller: controller == nil ? me.df_controller : controller,
			element: group.createChild(
				me.element_type, me.element_id
			),
		};
		append(m.parents, m.element);
		Symbol._new(m);

		m.init();
		return m;
	},
	del: func() {
		printlog(_MP_dbg_lvl, "DotSym.del()");
		me.deinit();
		call(Symbol.del, nil, me);
		me.element.del();
	},
# Default wrappers:
	init: func() me.draw(),
	deinit: func(),
	update: func() {
		if (me.controller != nil) {
			if (!me.controller.update(me, 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("DotSym.update(): bad position: "~debug.dump(pos));
		# print(me.model.id, ": Position lat/lon: ", lat, "/", lon);
		me.element.setGeoPosition(lat,lon);
		if (rotation != nil)
			me.element.setRotation(rotation);
	},
}; # of DotSym

##
# Small wrapper for DotSym: parse a SVG on init().
#
var SVGSymbol = {
	parents:[DotSym],
	element_type: "group",
	cacheable: 0,
	init: func() {
		if (!me.cacheable) {
			canvas.parsesvg(me.element, me.svg_path);
			# hack:
			if (var scale = me.layer.style['scale_factor'])
				me.element.setScale(scale);
		} else {
			__die("cacheable not implemented yet!");
		}
		me.draw();
	},
	draw: func,
}; # of SVGSymbol


##
# wrapper for symbols based on raster images (i.e. PNGs)
# TODO: generalize this and port WXR.symbol accordingly
#
var RasterSymbol = {
	parents:[DotSym],
	element_type: "group",
	cacheable: 0,
	size: [32,32], scale: 1,
	init: func() {
		if (!me.cacheable) {
			me.element.createChild("image", me.name)
			.setFile(me.file_path)
			.setSize(me.size)
			.setScale(me.scale);
		} else {
			__die("cacheable not implemented yet!");
		}
		me.draw();
	},
	draw: func,

}; # of RasterSymbol



var LineSymbol = {
	parents:[Symbol],
	element_id: nil,
	needs_update: 1,
# Static/singleton:
	makeinstance: func(name, hash) {
		if (!isa(hash, LineSymbol))
			__die("LineSymbol: OOP error");
		return Symbol.add(name, hash);
	},
# For the instances returned from makeinstance:
	new: func(group, layer, model, controller=nil) {
		if (me == nil) __die("Need me reference for LineSymbol.new()");
		if (typeof(model) != 'vector') __die("LineSymbol.new(): need a vector of points");
		var m = {
			parents: [me],
			group: group,
			layer: layer,
			model: model,
			controller: controller == nil ? me.df_controller : controller,
			element: group.createChild(
				"path", me.element_id
			),
		};
		append(m.parents, m.element);
		Symbol._new(m);

		m.init();
		return m;
	},
# Non-static:
	draw: func() {
		if (!me.needs_update) return;
		printlog(_MP_dbg_lvl, "redrawing a LineSymbol "~me.layer.type);
		me.element.reset();
		var cmds = [];
		var coords = [];
		var cmd = canvas.Path.VG_MOVE_TO;
		foreach (var m; me.model) {
			var (lat,lon) = me.controller.getpos(m);
			append(coords,"N"~lat);
			append(coords,"E"~lon);
			append(cmds,cmd); cmd = canvas.Path.VG_LINE_TO;
		}
		me.element.setDataGeo(cmds, coords);
		me.element.update(); # this doesn't help with flickering, it seems
	},
	del: func() {
		printlog(_MP_dbg_lvl, "LineSymbol.del()");
		me.deinit();
		call(Symbol.del, nil, me);
		me.element.del();
	},
# Default wrappers:
	init: func() me.draw(),
	deinit: func(),
	update: func() {
		if (me.controller != nil) {
			if (!me.controller.update(me, me.model)) return;
			elsif (!me.controller.isVisible(me.model)) {
				me.element.hide();
				return;
			}
		} else
		me.element.show();
		me.draw();
	},
}; # of LineSymbol

##
# Base class for a SymbolLayer, e.g. MultiSymbolLayer or SingleSymbolLayer.
#
var SymbolLayer = {
# Default implementations/values:
	df_controller: nil, # default controller
	df_priority: nil, # default priority for display sorting
	df_style: nil,
	df_options: nil,
	type: nil, # type of #Symbol to add (MANDATORY)
	id: nil, # id of the group #canvas.Element (OPTIONAL)
# Static/singleton:
	registry: {},
	add: func(type, class)
		me.registry[type] = class,
	get: func(type) {
		foreach(var invalid; var invalid_types = [nil,'vector','hash'])
			if ( (var t=typeof(type)) == invalid) __die(" invalid SymbolLayer type (non-scalar) of type:"~t);
		if ((var class = me.registry[type]) == nil)
			__die("SymbolLayer.get(): unknown type '"~type~"'");
		else return class;
	},
	# Calls corresonding layer constructor
	# @param group #Canvas.Group to place this on.
	# @param map The #Canvas.Map this is a member of.
	# @param controller A controller object.
	# @param style An alternate style.
	# @param options Extra options/configurations.
	# @param visible Initially set it up as visible or invisible.
	new: func(type, group, map, controller=nil, style=nil, options=nil, visible=1, arg...) {
		# XXX: Extra named arguments are (obviously) not preserved well...
		var ret = call((var class = me.get(type)).new, [group, map, controller, style, options, visible]~arg, class);
		ret.group.set("layer-type", type);
		return ret;
	},
	# Private constructor:
	_new: func(m, style, controller, options) {
		# print("SymbolLayer setup options:", m.options!=nil);
		m.style = default_hash(style, m.df_style);
		m.options = default_hash(options, m.df_options);
		
		if (controller == nil)
			controller = m.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("MultiSymbolLayer: OOP error");
		m.controller = controller;
	},
# For instances:
	del: func() if (me.controller != nil) { me.controller.del(); me.controller = nil },
	update: func() __die("Abstract SymbolLayer.update() not implemented for this Layer"),
};

# 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),
# Default implementations for derived classes:
	# @return List of positioned objects.
	searchCmd: func()
		__die("Abstract method searchCmd() not implemented for this SymbolLayer.Controller type!"),
	addVisibilityListener: func() {
		var m = me;
		append(m.listeners, setlistener(
			m.layer._node.getNode("visible"),
			func m.layer.update(),
			#compile("m.layer.update()", "<layer visibility on node "~m.layer._node.getNode("visible").getPath()~" for layer "~m.layer.type~">"),
			0,0
		));
	},
# Default implementations for derived objects:
	# For SingleSymbolLayer: retreive the model object
	getModel: func me._model, # assume they store it here - otherwise they can override this
}; # of SymbolLayer.Controller

##
# A layer that manages a list of symbols (using delta positioned handling
# with a searchCmd to retreive placements).
#
var MultiSymbolLayer = {
	parents: [SymbolLayer],
# Default implementations/values:
	# @param group A group to place this on.
	# @param map The #Canvas.Map this is a member of.
	# @param controller A controller object (parents=[SymbolLayer.Controller])
	#                   or implementation (parents[0].parents=[SymbolLayer.Controller]).
	# @param style An alternate style.
	# @param options Extra options/configurations.
	# @param visible Initially set it up as visible or invisible.
	new: func(group, map, controller=nil, style=nil, options=nil, visible=1) {
		#print("Creating new SymbolLayer instance");
		if (me == nil) __die("MultiSymbolLayer constructor needs to know its parent class");
		var m = {
			parents: [me],
			map: map,
			group: group.createChild("group", me.type),
			list: [],
		};
		append(m.parents, m.group);
		m.setVisible(visible);
		# N.B.: this has to be here for the controller
		m.searcher = geo.PositionedSearch.new(me.searchCmd, me.onAdded, me.onRemoved, m);
		SymbolLayer._new(m, style, controller, options);

		m.update();
		return m;
	},
	update: func() {
		if (!me.getVisible())
			return;
		#debug.warn("update traceback for "~me.type);

		var updater = func {
			me.searcher.update();
			foreach (var e; me.list)
				e.update();
		}

		if (me.options != nil and me.options['update_wrapper'] !=nil) {
			me.options.update_wrapper( me, updater ); # call external wrapper (usually for profiling purposes)
		} else {
			updater();
		}
	},
	del: func() {
		printlog(_MP_dbg_lvl, "MultiSymbolLayer.del()");
		foreach (var e; me.list)
			e.del();
		call(SymbolLayer.del, nil, me);
	},
	delsym: func(model) {
		forindex (var i; me.list) {
			var e = me.list[i];
			if (Symbol.Controller.equals(e.model, model)) {
				# Remove this element from the list
				# TODO: maybe C function for this? extend pop() to accept index?
				var prev = subvec(me.list, 0, i);
				var next = subvec(me.list, i+1);
				me.list = prev~next;
				e.del();
				return 1;
			}
		}
		return 0;
	},
	searchCmd: func() { 
		if (me.map.getPosCoord() == nil or me.map.getRange() == nil) { 
			print("Map not yet initialized, returning empty result set!");
			return []; # handle maps not yet fully initialized
		}
		var result = me.controller.searchCmd();
		# some hardening
		var type=typeof(result);
		if(type != 'nil' and type != 'vector') 
			__die("MultiSymbolLayer: searchCmd() method MUST return a vector of valid positioned ghosts/Geo.Coord objects or nil! (was:"~type~")");
		return result;
	},
	# Adds a symbol.
	onAdded: func(model) {
		printlog(_MP_dbg_lvl, "Adding symbol of type "~me.type);
		if (model == nil) __die("MultiSymbolLayer: Model was nil for layer:"~debug.string(me.type)~ " Hint:check your equality check method!");
		append(me.list, Symbol.new(me.type, me.group, me, model));
	},
	# Removes a symbol.
	onRemoved: func(model) {
		printlog(_MP_dbg_lvl, "Deleting symbol of type "~me.type);
		if (!me.delsym(model)) __die("model not found");
		try_aux_method(model, "del");
		#call(func model.del(), nil, var err = []); # try...
		#if (size(err) and err[0] != "No such member: del") # ... and either catch or rethrow
		#	die(err[0]);
	},
}; # of MultiSymbolLayer

##
# A layer that manages a list of statically-positioned navaid symbols (using delta positioned handling
# with a searchCmd to retrieve placements).
# This is not yet supposed to work properly, it's just there to help get rid of all the identical boilerplate code
# in lcontroller files, so needs some reviewing and customizing still
#
var NavaidSymbolLayer = {
	parents: [MultiSymbolLayer],
# static generator/functor maker:
	make: func(query) {
		#print("Creating searchCmd() for NavaidSymbolLayer:", query);
		return func {
			printlog(_MP_dbg_lvl, "Running query:", query);
			var range = me.map.getRange();
			if (range == nil) return;
			return positioned.findWithinRange(me.map.getPosCoord(), range, query);
		};
	},
}; # of NavaidSymbolLayer



###
## TODO: wrappers for Horizontal vs. Vertical layers ?
## 

var SingleSymbolLayer = {
	parents: [SymbolLayer],
# Default implementations/values:
	# @param group A group to place this on.
	# @param map The #Canvas.Map this is a member of.
	# @param controller A controller object (parents=[SymbolLayer.Controller])
	#                   or implementation (parents[0].parents=[SymbolLayer.Controller]).
	# @param style An alternate style.
	# @param options Extra options/configurations.
	# @param visible Initially set it up as visible or invisible.
	new: func(group, map, controller=nil, style=nil, options=nil, visible=1) {
		#print("Creating new SymbolLayer instance");
		if (me == nil) __die("SingleSymbolLayer constructor needs to know its parent class");
		var m = {
			parents: [me],
			map: map,
			group: group.createChild("group", me.type),
		};
		append(m.parents, m.group);
		m.setVisible(visible);
		SymbolLayer._new(m, style, controller, options);

		m.symbol = Symbol.new(m.type, m.group, m, m.controller.getModel());
		m.update();
		return m;
	},
	update: func() {
		if (!me.getVisible())
			return;

		var updater = func {
			if (typeof(me.symbol.model) == 'hash') try_aux_method(me.symbol.model, "update");
			me.symbol.update();
		}

		if (me.options != nil and me.options['update_wrapper'] != nil) {
			me.options.update_wrapper( me, updater ); # call external wrapper (usually for profiling purposes)
		} else {
			updater();
		}
	},
	del: func() {
		printlog(_MP_dbg_lvl, "SymbolLayer.del()");
		me.symbol.del();
		call(SymbolLayer.del, nil, me);
	},
}; # of SingleSymbolLayer

###
# set up a cache for 32x32 symbols (initialized below in load_MapStructure)
var SymbolCache32x32 = nil;

var load_MapStructure = func {
	canvas.load_MapStructure = func; # disable any subsequent attempt to load

	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, map, arg...) {
			var m = call((var class = me.get(type)).new, [map]~arg, class);
			if (!contains(m, "map"))
				m.map = map;
			elsif (m.map != map and !isa(m.map, map) and (
			        m.get_position != Map.Controller.get_position
			     or m.query_range != Map.Controller.query_range
			     or m.in_range != Map.Controller.in_range))
			{ __die("m must store the map handle as .map if it uses the default method(s)"); }
		},
	# Default implementations:
		get_position: func() {
			debug.warn("get_position is deprecated");
			return me.map.getLatLon()~[me.map.getAlt()];
		},
		query_range: func() {
			debug.warn("query_range is deprecated");
			return me.map.getRange() or 30;
		},
		in_range: func(lat, lon, alt=0) {
			var range = me.map.getRange();
			if(range == nil) __die("in_range: Invalid query range!");
			# print("Query Range is:", range );
			if (lat == nil or lon == nil) __die("in_range: lat/lon invalid");
			var pos = geo.Coord.new();
			pos.set_latlon(lat, lon, alt or 0);
			var map_pos = me.map.getPosCoord();
			if (map_pos == nil)
				return 0; # should happen *ONLY* when map is uninitialized
			var distance_m = pos.distance_to( map_pos );
			var is_in_range = distance_m < range * NM2M;
			# print("Distance:",distance_m*M2NM," nm in range check result:", is_in_range);
			return is_in_range;
		},
	};

	####### LOAD FILES #######
	(func {
		var FG_ROOT = getprop("/sim/fg-root");
		var load = func(file, name) {
			if (name == nil)
				var name = split("/", file)[-1];
			var code = io.readfile(file);
			var code = call(func compile(code, file), [code], var err=[]);
			if (size(err)) {
				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;
			}
			#code=bind(
			call(code, nil, nil, var hash = {});

			# validate
			var url = ' http://wiki.flightgear.org/MapStructure#';
			# TODO: these rules should be extended for all main files lcontroller/scontroller and symbol
			var checks = [
					{ extension:'symbol', symbol:'update', type:'func', error:' update() must not be overridden:', id:300},
					# Sorry, this one doesn't work with the new LineSymbol
#					{ extension:'symbol', symbol:'draw', type:'func', required:1, error:' symbol files need to export a draw() routine:', id:301},
					# Sorry, this one doesn't work with the new SingleSymbolLayer
#					{ extension:'lcontroller', symbol:'searchCmd', type:'func', required:1, error:' lcontroller without searchCmd method:', id:100},
					];


			var makeurl = func(scope, id) url ~ scope ~ ':' ~ id;
			var bailout = func(file, message, scope, id) __die(file~message~"\n"~makeurl(scope,id) );

			var current_ext = split('.', file)[-1];
			foreach(var check; checks) {
				# check if we have any rules matching the current file extension
				if (current_ext == check.extension) {
					# check for fields that must not be overridden
					if (check['error'] != nil and 
						hash[check.symbol]!=nil and !check['required']  and 
						typeof(hash[check.symbol])==check.type ) {
						bailout(file,check.error,check.extension,check.id);
					}

					# check for required fields
					if (check['required'] != nil and 
						hash[check.symbol]==nil and
						typeof( hash[check.symbol]) != check.type) {
						bailout(file,check.error,check.extension,check.id);
					}
				}
			}

			return hash;
		};

		# sets up a shared symbol cache, which will be used by all MapStructure maps and layers
		canvas.SymbolCache32x32 = SymbolCache.new(1024,32);

		# Find files and load them:
		var contents_dir = FG_ROOT~"/Nasal/canvas/map/";
		var dep_names = [
			# With these extensions, in this order:
			"lcontroller",
			"symbol",
			"scontroller",
			"controller",
		];
		var deps = {};
		foreach (var d; dep_names) deps[d] = [];
		foreach (var f; directory(contents_dir)) {
			var ext = size(var s=split(".", f)) > 1 ? s[-1] : nil;
			foreach (var d; dep_names) {
				if (ext == d) {
					append(deps[d], f);
					break
				}
			}
		}
		foreach (var d; dep_names) {
			foreach (var f; deps[d]) {
				var name = split(".", f)[0];
				load(contents_dir~f, name);
			}
		}
	})();

}; # load_MapStructure

setlistener("/nasal/canvas/loaded", load_MapStructure); # end ugly module init listener hack. FIXME: do smart Nasal bootstrapping, quod est callidus!
# 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