1
0
Fork 0
fgdata/Nasal/canvas/map.nas
Hooray e510c8917f Canvas Scripting Layer (Mapping):
- first stab at refactoring the map.nas module, and trying to let the API evolve according to our requirements
- split up the module into separate files (some of them will disappear soon)
- split up the "drawing" loops into separate functions so that they can be individually called
- move actual "drawing" to map_layers.nas
- introduce some OOP helpers to prepare a pure Layer-based design
- prepare helpers: LayeredMap, GenericMap, AirportMap (TODO: use a real "Layer" class)
- move airport features (taxiways, runways, parking, tower) to separate layers (i.e. canvas groups)
- avoid using a single update callback and use different layer-specific callbacks to update individual layers more efficiently
- add some boilerplate hashes to prepare the MVC design
- allow lazy updating of layers, where canvas groups are only populated on demand, to save some time during instantiation, i.e. loading an airport without "parking" selected, will only populate the layer once the checkbox is checked
- extend the original code such that it supports showing multiple airports at once
- add some proof of concept "navaid" layer using SVG files for navaid symbols (added only NDB symbol from wikimedia commons)

regressions:
- runway highlighting needs to be re-implemented
- parking highlighting will be done differently
- enforcing a specific drawing order for layers is currently not explicitly supported, so that taxiways may be rendered on top of runways

Also:

- integrated with the latest changes in git/master (HEAD) -i.e. metar support
- further generalized map.nas
- partially moved instantiation from Nasal space to XML space (WIP)
- create "toggle layer" checkboxes procedurally in Nasal space
- prepared the code to be better reusable in other dialogs (e.g. route manager, map dialog etc)
- completely removed the "highlighting" (runway/parking) feature for now, because we talked about re-implementing it anyhow
2012-09-29 21:16:31 +01:00

580 lines
20 KiB
Text

###
# map.nas - provide a high level method to create typical maps in FlightGear (airports, navaids, fixes and waypoints) for both, the GUI and instruments
# implements the notion of a "layer" by using canvas groups and adding geo-referenced elements to a layer
# layered maps are linked to boolean properties so that visibility can be easily toggled (GUI checkboxes or cockpit hotspots)
# without having to redraw other layers
#
# GOALS: have a single Nasal/Canvas wrapper for all sort of maps in FlightGear, that can be easily shared and reused for different purposes
#
# DESIGN: ... is slowly evolving, but still very much beta for the time being
#
# API: not yet documented, but see eventually design.txt (will need to add doxygen-strings then)
#
# PERFORMANCE: will be improved, probabaly by moving some features to C++ space and optimizing things there
#
#
# ISSUES: just look for the FIXME and TODO strings - currently, the priority is to create an OOP/MVC design with less specialized code in XML files
#
#
# ROADMAP: Generalize this further, so that:
#
# - it can be easily reused
# - use a MVC approach, where layer-specific data is provided by a Model object
# - other dialogs can use this without tons of custom code (airports.xml, route-manager.xml, map-canvas.xml)
# - generalize this further so that it can be used by instruments
# - implement additional layers (tcas, wxradar, agradar) - especially expose the required data to Nasal
# - implement better GUI support (events) so that zooming/panning via mouse can be supported
# - make the whole thing styleable
#
# - keep track of things getting added here and decide if they should better move to the core canvas module or the C++ code
#
#
# C++ RFEs:
# - overload findNavaidsWithinRange() to support an optional position argument, so that arbitrary navaids can be looked up
# - add Nasal extension function to get scenery vector data (landclass)
# -
# -
#
var DEBUG=0;
if (DEBUG) {
var benchmark = debug.benchmark;
}
else {
var benchmark = func(label, code) code(); # NOP
}
var assert = func(label, expr) expr and die(label);
# Mapping from surface codes to #TODO: make this XML-configurable
var SURFACECOLORS = {
1 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
2 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
3 : { type: "turf", r:0.2, g:0.5, b:0.2 },
4 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
5 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
# Helipads
6 : { type: "asphalt", r:0.2, g:0.2, b:0.2 },
7 : { type: "concrete", r:0.3, g:0.3, b:0.3 },
8 : { type: "turf", r:0.2, g:0.5, b:0.2 },
9 : { type: "dirt", r:0.4, g:0.3, b:0.3 },
0 : { type: "gravel", r:0.35, g:0.3, b:0.3 },
};
###
# ALL LayeredMap "draws" go through this wrapper, which makes it easy to check what's going on:
var draw_layer = func(layer, callback, lod) {
var name= layer._view.get("id");
# print("Canvas:Draw op triggered"); # just to make sure that we are not adding unnecessary data when checking/unchecking a checkbox
if (DEBUG and name=="taxiways") fgcommand("profiler-start"); #without my patch, this is a no op, so no need to disable
#print("Work items:", size(layer._model._elements));
benchmark("Drawing Layer:"~layer._view.get("id"), func
foreach(var element; layer._model._elements) {
#print(typeof(layer._view));
#debug.dump(layer._view);
callback(layer._view, element, lod); # ISSUE here
});
if (! layer._model.hasData() ) print("Layer was EMPTY:", name);
if (DEBUG and name=="taxiways") fgcommand("profiler-stop");
layer._drawn=1; #TODO: this should be encapsulated
}
# Runway
#
var Runway = {
# Create Runway from hash
#
# @param rwy Hash containing runway data as returned from
# airportinfo().runways[ <runway designator> ]
new: func(rwy)
{
return {
parents: [Runway],
rwy: rwy
};
},
# Get a point on the runway with the given offset
#
# @param pos Position along the center line
# @param off Offset perpendicular to the center line
pointOffCenterline: func(pos, off = 0)
{
var coord = geo.Coord.new();
coord.set_latlon(me.rwy.lat, me.rwy.lon);
coord.apply_course_distance(me.rwy.heading, pos - 0.5 * me.rwy.length);
if( off )
coord.apply_course_distance(me.rwy.heading + 90, off);
return ["N" ~ coord.lat(), "E" ~ coord.lon()];
}
};
var make = func return {parents:arg};
##
# TODO: Create a cache to reuse layers and layer data (i.e. runways)
##
# Todo: wrap parsesvg and return a function that memoizes the created canvas group, so that svg files only need to be parsed once
#
##
# TODO: Implement a real MVC design for "LayeredMaps" that have:
# - a "DataProvider" (i.e. Positioned objects)
# - a View (i.e. a Canvas)
# - a controller (i.e. input/output properties)
#
var MapModel = {}; # navaids, waypoints, fixes etc
MapModel.new = func make(MapModel);
var MapView = {}; # the canvas view, including a layer for each feature
MapView.new = func make(MapView);
var MapController = {}; # the property tree interface to manipulate the model/view via properties
MapController.new = func make(MapController);
var LazyView = {}; # Gets drawables on demand from the model - via property toggle
var DataProvider = {};
DataProvider.new = func make(DataProvider);
###
# for airports, navaids, fixes, waypoints etc
var PositionedProvider = {};
PositionedProvider.new = func make(DataProvider, PositionedProvider);
##
# Drawable
#
## LayerElement (UNUSED ATM):
# for runways, navaids, fixes, waypoints etc
# TODO: we should differentiate between "fairly static" vs. "dynamic" layers - i.e. navaids vs. traffic
var LayerElement = {_drawable:nil};
LayerElement.new = func(drawable) {
var temp = make(LayerElement);
temp._drawable=drawable;
return temp;
}
# a drawable is either a Nasal callback or a scalar, i.e. a path to an SVG file
LayerElement.draw = func(group) {
(typeof(me._drawable)=='func') and drawable(group) or canvas.parsesvg(group,_drawable);
}
# For static targets like Navaids, Fixes - i.e. geographic position doesn't change
var StaticLayerElement = {};
# For moving targets such as aircraft, multiplayer, ai traffic etc
var DynamicLayerElement = {};
var AnimatedLayerElement = {};
# for elements whose appearance may change depending on selected range (i.e. LOD)
var RangeAwareLayerElement = {};
##
# A layer model is just a wrapper for a vector with elements
# either updated via a timer or via a listener
var LayerModel = {_elements:[], _view:, _controller: };
LayerModel.new = func make(LayerModel);
LayerModel.clear = func me._elements = [];
LayerModel.push = func (e) append(me._elements, e);
LayerModel.get = func me._elements;
LayerModel.update = func;
LayerModel.hasData = func size(me. _elements);
LayerModel.setView = func(v) me._view=v;
LayerModel.setController = func(c) me._controller=c;
var LayerController = {};
LayerController.new = func make(LayerController);
##
# use timers to update the model/view (canvas)
var TimeBasedLayerController = {};
LayerController.new = func make(TimeBasedLayerController);
##
# use listeners to update the model/view (canvas)
#
var ListenerBasedLayerController = {};
ListenerBasedLayerController.new = func make(ListenerBasedLayerController);
##
# Uses, both, listeners and timers to update the model/view (canvas)
#
var HybridLayerController = {};
HybridLayerController.new = func make(HybridLayerController);
var ModelEvents = {INIT:, RESET:, UPDATE:};
var ViewEvents = {INIT:, RESET:, UPDATE:};
var ControllerEvents = {INIT:, RESET: , UPDATE:, ZOOM:, PAN:, };
##
# A layer is mapped to a canvas group
# Layers are linked to a single boolean property to toggle them on/off
var Layer = { _model: ,
_view: ,
_controller: ,
_drawn:0,
};
Layer.new = func(group, name, model) {
#print("Setting up new Layer:", name);
var m = make(Layer);
m._model = model.new();
#print("Model name is:", m._model.name);
m._view = group.createChild("group",name);
m.name = name; #FIXME: not needed, there's already _view.get("id")
return m;
}
Layer.hide = func me._view.setVisible(0);
Layer.show = func me._view.setVisible(1);
#TODO: Unify toggle and update methods - and support lazy drawing (make it optional!)
Layer.toggle = func {
# print("Toggling layer");
var checkbox = getprop(me.display_layer);
if(checkbox and !me._drawn) {
# print("Lazy drawing");
me.draw();
}
#var state= me._view.getBool("visible");
#print("Toggle layer visibility ",me.display_layer," checkbox is", checkbox);
#print("Layer id is:", me._view.get("id"));
#print("Drawn is:", me._drawn);
checkbox?me._view.setVisible(1) : me._view.setVisible(0);
}
Layer.reset = func {
me._view.removeAllChildren(); # clear the "real" canvas drawables
me._model.clear(); # the vector is used for lazy rendering
assert("Model not emptied during layer reset!", me._model.hasData() );
me._drawn = 0;
}
#TODO: Unify toggle and update
Layer.update = func {
# print("Layer update: Check if layer is visible, if so, draw");
if (! getprop(me.display_layer)) return; # checkbox for layer not set
if (!me._model.hasData() ) return; # no data available
# print("Trying to draw");
me.draw();
}
Layer.setDraw = func(callback) me.draw = callback;
Layer.setController = func(c) me._controller=c; # TODO: implement
Layer.setModel = func(m) nil; # TODO: implement
##TODO: differentiate between layers with a single object (i.e. aircraft) and multiple objects (airports)
##
# We may need to display some stuff that isn't strictly a geopgraphic feature, but just a chart feature
#
var CartographicLayer = {};
#TODO:
var InteractiveLayer = {};
###
# PositionedLayer
#
# layer of positioned objects (i.e. have lat,lon,alt)
#
var PositionedLayer = {};
PositionedLayer.new = func() {
make( Layer.new() , PositionedLayer );
}
###
# CachedLayer
#
# when re-centering on an airport already loaded, we don't want to reload it
# but change the reference point and load missing airports
var CachedLayer = {};
##
#
var AirportProvider = {};
AirportProvider.new = func make(AirportProvider);
AirportProvider.get = func {
return airportinfo("ksfo");
}
### Data Providers (preparation for MVC version):
# TODO: should use the LayerModel class
#
##
# Manage a bunch of layers
#
var LayerManager = {};
# WXR ?
# TODO: Stub
var MapBehavior = {};
MapBehavior.new = make(MapBehavior);
MapBehavior.zoom = func;
MapBehavior.center = func;
##
# A layered map consists of several layers
# TODO: Support nested LayeredMaps, where a LayeredMap may contain other LayeredMaps
# TODO: use MapBehavior here and move the zoom/refpos methods there, so that map behavior can be easily customized
var LayeredMap = { ranges:[],
zoom_property:nil, listeners:[],
update_property:nil, layers:[],
};
LayeredMap.new = func(parent, name)
return make(LayeredMap, parent.createChild("map",name) );
LayeredMap.listen = func(p,c) { #FIXME: listening should be managed by each m/v/c separately
# print("Setting up LayeredMap-managed listener:", p);
append(me.listeners, setlistener(p, c));
}
LayeredMap.initializeLayers = func {
# print("initializing all layers and updating");
foreach(var l; me.layers)
l.update();
}
LayeredMap.setRefPos = func(lat, lon) {
# print("RefPos set");
me._node.getNode("ref-lat", 1).setDoubleValue(lat);
me._node.getNode("ref-lon", 1).setDoubleValue(lon);
me; # chainable
}
LayeredMap.setHdg = func(hdg) {
me._node.getNode("hdg",1).setDoubleValue(hdg);
me; # chainable
}
LayeredMap.updateZoom = func {
var z = getprop(me.zoom_property) or 0;
var zoom = me.ranges[ size(me.ranges)-1 -z];
# print("Setting zoom range to:", zoom);
benchmark("Zooming map:"~zoom, func
me._node.getNode("range", 1).setDoubleValue(zoom)
);
me; #chainable
}
# this is a huge hack at the moment, we need to encapsulate the setRefPos/setHdg methods, so that they are exposed to XML space
#
LayeredMap.updateState = func {
# center map on airport TODO: should be moved to a method and wrapped with a controller so that behavior can be customizeda
#var apt = me.layers[0]._model._elements[0];
# FIXME:
#me.setRefPos(lat:me._refpos.lat, lon:me._refpos.lon);
me.setHdg(0.0);
me.updateZoom();
}
#
# TODO: this is currently GUI specific and not re-usable for instruments
LayeredMap.setupZoom = func(dialog) {
var dlgroot = dialog.getNode("features/dialog-root").getValue();#FIXME: GUI specific - needs to be re-implemented for instruments
var zoom_property = dlgroot ~"/"~dialog.getNode("features/range-property").getValue(); #FIXME: this doesn't belong here, need to be in ctor instead !!!
ranges=dialog.getNode("features/ranges").getChildren("range");
foreach(var r; ranges)
append(me.ranges, r.getValue() );
# print("Setting up Zoom Ranges:", size(ranges)-1);
me.zoom_property=zoom_property;
me.listen(zoom_property, func me.updateZoom() );
me.updateZoom();
me; #chainable
}
LayeredMap.setZoom = func {} #TODO
LayeredMap.resetLayers = func {
benchmark("Resetting LayeredMap", func
foreach(var l; me.layers) { #TODO: hide all layers, hide map
l.reset();
}
);
}
#FIXME: listener management should be done at the MVC level, for each component - not as part of the LayeredMap!
LayeredMap.cleanup_listeners = func {
print("Cleaning up listeners");
foreach(var l; me.listeners)
removelistener(l);
}
###
# GenericMap: A generic map is a layered map that puts all supported features on a different layer (canvas group) so that
# they can be individually toggled on/off so that unnecessary updates are avoided, there are methods to link layers to boolean properties
# so that they can be easily associated with GUI properties (checkboxes) or cockpit hotspots
# TODO: generalize the XML-parametrization and move it to a helper class
var GenericMap = { };
GenericMap.new = func(parent, name) make(LayeredMap.new(parent:parent, name:name), GenericMap);
GenericMap.setupLayer = func(layer, property) {
var l = MAP_LAYERS[layer].new(me, layer); # Layer.new(me, layer);
l.display_layer = property; #FIXME: use controller object instead here and this overlaps with update_property
#print("Set up layer with toggle property=", property);
l._view.setVisible( getprop(property) ) ;
append(me.layers, l);
return l;
}
# features are layers - so this will do layer setup and then register listeners for each layer
GenericMap.setupFeature = func(layer, property, init ) {
var l=me.setupLayer( layer, property );
me.listen(property, func l.toggle() ); #TODO: should use the controller object here !
l._model._update_property=property; #TODO: move somewhere else - this is the property that is mapped to the CHECKBOX
l._model._view_handle = l; #FIXME: very crude, set a handle to the view(group), so that the model can notify it (for updates)
l._model._map_handle = me; #FIXME: added here so that layers can send update requests to the parent map
#print("Setting up layer init for property:", init);
l._model._input_property = init; # FIXME: init property = input property - needs to be improved!
me.listen(init, func l._model.init() ); #TODO: makes sure that the layer's init method for the MODEL is invoked
me; #chainable
};
# This will read in the config and procedurally instantiate all requested layers and link them to toggle properties
# FIXME: this is currently GUI specific and doesn't yet support instrument use, i.e. needs to be generalized further
GenericMap.pickupFeatures = func(DIALOG_CANVAS) {
var dlgroot = DIALOG_CANVAS.getNode("features/dialog-root").getValue();
# print("Picking up features for:", DIALOG_CANVAS.getPath() );
var layers=DIALOG_CANVAS.getNode("features").getChildren("layer");
foreach(var n; layers) {
var name = n.getNode("name").getValue();
var toggle = n.getNode("property").getValue();
var init = n.getNode("init-property").getValue();
init = dlgroot ~"/"~init;
var property = dlgroot ~"/"~toggle;
# print("Adding layer:",n.getNode("name").getValue() );
me.setupFeature(name, property, init);
}
me;
}
# NOT a method, cmdarg() is no longer meaningful when the canvas nasal block is executed
# so this needs to be called in the dialog's OPEN block instead - TODO: generalize
#FIXME: move somewhere else, this is a GUI helper and should probably be generalized and moved to gui.nas
GenericMap.setupGUI = func (dialog, group) {
var group = gui.findElementByName(cmdarg() , group);
var layers=dialog.getNode("features").getChildren("layer");
var template = dialog.getNode("checkbox-toggle-template");
var dlgroot = dialog.getNode("features/dialog-root").getValue();
var zoom = dlgroot ~"/"~ dialog.getNode("features/range-property").getValue();
var i=0;
foreach(var n; layers) {
var name = n.getNode("name").getValue();
var toggle = dlgroot ~ "/" ~ n.getNode("property").getValue();
var label = n.getNode("description",1).getValue() or name;
var default = n.getNode("default",1).getValue();
default = (default=="enabled")?1:0;
#print("Layer default for", name ," is:", default);
setprop(toggle, default); # set the checkbox to its default setting
var hide_checkbox = n.getNode("hide-checkbox",1).getValue();
hide_checkbox = (hide_checkbox=="true")?1:0;
var checkbox = group.getChild("checkbox",i, 1); #FIXME: compute proper offset dynamically, will currently overwrite other existing checkboxes!
props.copy(template, checkbox);
checkbox.getNode("name").setValue("display-"~name);
checkbox.getNode("label").setValue(label);
checkbox.getNode("property").setValue(toggle);
checkbox.getNode("binding/object-name").setValue("display-"~name);
checkbox.getNode("enabled",1).setValue(!hide_checkbox);
i+=1;
}
#add zoom buttons procedurally:
var template = dialog.getNode("zoom-template");
template.getNode("button[0]/binding[0]/property[0]").setValue(zoom);
template.getNode("text[0]/property[0]").setValue(zoom);
template.getNode("button[1]/binding[0]/property[0]").setValue(zoom);
template.getNode("button[1]/binding[0]/max[0]").setValue( i );
props.copy(template, group);
}
###
# TODO: StylableGenericMap (colors, fonts, symbols)
#
var AirportMap = {};
AirportMap.new = func(parent,name) make(GenericMap.new(parent,name), AirportMap);
#TODO: Use real MVC (DataProvider/PositionedProvider) here
# this is currently "directly" invoked via a listener, needs to be changed
# to use the controller object instead
# TODO: adopt real MVC here
# FIXME: this must currently be explicitly called by the model, we need to use a wrapper to call it automatically instead!
LayerModel.notifyView = func () {
# print("View notified");
me._view_handle.update(); # update the layer/group
me._map_handle.updateState(); # update the map
}
# ID
var SingleAirportProvider = {};
# inputs: position, range
var MultiAirportProvider = {};
#TODO: remove and unify with update()
AirportMap.init = func {
me.resetLayers();
me.updateState();
}
# MultiObjectLayer:
# - Airports
# - Traffic (MP/AI)
# - Navaids
#
# TODO: a "MapLayer" is a full MVC implementation that is owned by a "LayeredMap"
var MAP_LAYERS = {};
var register_layer = func(name, layer) MAP_LAYERS[name]=layer;
var MVC_FOLDER = getprop("/sim/fg-root") ~ "/Nasal/canvas/map/";
var load_modules = func(vec) foreach(var file; vec) io.load_nasal(MVC_FOLDER~file, "canvas");
# TODO: read in the file names dynamically: *.draw, *.model, *.layer
var DRAWABLES = ["navaid.draw", "parking.draw", "runways.draw", "taxiways.draw", "tower.draw"];
load_modules(DRAWABLES);
var MODELS = ["airports.model", "navaids.model",];
load_modules(MODELS);
var LAYERS = ["runways.layer", "taxiways.layer", "parking.layer", "tower.layer", "navaids.layer","test.layer",];
load_modules(LAYERS);
#TODO: Implement!
var CONTROLLERS = [];
load_modules(CONTROLLERS);