Board index FlightGear Development Canvas

Designing the new map.nas API

Canvas is FlightGear's new fully scriptable 2D drawing system that will allow you to easily create new instruments, HUDs and even GUI dialogs and custom GUI widgets, without having to write C++ code and without having to rebuild FlightGear.

Designing the new map.nas API

Postby Hooray » Mon Sep 17, 2012 6:38 pm

Once this is committed, it would probably be a good idea to review the Nasal code and see how it can be generalized, there are going to be a number of uses for this, such as re-implementing the map dialog, the airport selection dialog, ground radar, ATC-FS etc - so these should ideally all use the same Nasal backend/hash and just parametrize it as required (style/behavior).

Like Zakalawe mentioned a while ago, we don't want to have tons of embedded Nasal code in XML dialog files, just because it's easy - custom widgets and display-types should be well encapsulated in distinct Nasal modules that can be easily maintained.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11427
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby Hooray » Tue Sep 18, 2012 9:17 am

https://gitorious.org/fg/fgdata/commit/ ... 52d1126bb8
@TheTom: I just looked through your changes, looks pretty good - most of the code (helpers, hashes) could be shared among similar users (map dialog, atc-fs etc) - so couldn't we directly move the code from the canvas <load> section to $FG_ROOT/Nasal/canvas/map.nas ?

That would at least help ensure that this part of the API will be further developed - because all similar uses could be generalized, so that the code is unified and moved to a separate canvas/maps.nas module.

Parametrization could then be either done via loading SVG files (instead of procedurally defining symbols) or simply by supporting a draw() callback, so that custom stuff can still be easily implemented.

We just need to have a clean way to support both modes: the "old/current" CanvasWidget approach, and the more modern "pure" Canvas mode which doesn't use any PUI at all.

Currently, the main difference will be how the Nasal code is invoked - i.e. via embedded Nasal sections, but that would be also simplified by having a single module that can be used to instantiate new objects.
Code: Select all
diff --git a/Nasal/canvas/map.nas b/Nasal/canvas/map.nas
new file mode 100644
index 0000000..8b51169
--- /dev/null
+++ b/Nasal/canvas/map.nas
@@ -0,0 +1,156 @@
+###
+# helpers
+
+   var Runway = {
+            new: func(rwy)
+            {
+              return {
+                parents: [Runway],
+                rwy: rwy
+              };
+            },
+            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()];
+            }
+          };
+         
+###
+# AirportMap class
+#
+          var AirportMap = {
+            new: func(apt)
+            {
+              return {
+                parents: [AirportMap],
+                _apt: apt
+              };
+            },
+            build: func(layer_runways, selected=nil)
+            {
+              var rws_done = {};
+
+              me.grp_apt = layer_runways.createChild("group", "apt-" ~ me._apt.id);
+              var selected_rwy = (selected) ? getprop(selected) : nil;           
+             
+              foreach(var rw; keys(me._apt.runways))
+              {
+                var is_heli = substr(rw, 0, 1) == "H";
+                var rw_dir = is_heli ? nil : int(substr(rw, 0, 2));
+               
+                var rw_rec = "";           
+                var thresh_rec = 0;
+                if( rw_dir != nil )
+                {
+                  rw_rec = sprintf("%02d", math.mod(rw_dir - 18, 36));
+                  if( size(rw) == 3 )
+                  {
+                    var map_rec = {
+                      "R": "L",
+                      "L": "R",
+                      "C": "C"
+                    };
+                    rw_rec ~= map_rec[substr(rw, 2)];
+                  }
+                 
+                  if( rws_done[rw_rec] != nil )
+                    continue;
+                 
+                  var rw_rec = me._apt.runways[rw_rec];
+                  if( rw_rec != nil )
+                    thresh_rec = rw_rec.threshold;
+                }
+
+                rws_done[rw] = 1;
+
+                rw = me._apt.runways[rw];
+                var icon_rw =
+                  me.grp_apt.createChild("path", "runway")
+                            .setStrokeLineWidth(0.5)
+                            .setColor(1.0,1.0,1.0)
+                            .setColorFill(0.2, 0.2, 0.2);
+
+                var rwy = Runway.new(rw);
+                var beg_thr  = rwy.pointOffCenterline(rw.threshold);
+                var beg_thr1 = rwy.pointOffCenterline(rw.threshold,  0.5 * rw.width);
+                var beg_thr2 = rwy.pointOffCenterline(rw.threshold, -0.5 * rw.width);
+                var beg1 = rwy.pointOffCenterline(0,  0.5 * rw.width);
+                var beg2 = rwy.pointOffCenterline(0, -0.5 * rw.width);
+               
+                var end_thr  = rwy.pointOffCenterline(rw.length - thresh_rec);
+                var end_thr1 = rwy.pointOffCenterline(rw.length - thresh_rec,  0.5 * rw.width);
+                var end_thr2 = rwy.pointOffCenterline(rw.length - thresh_rec, -0.5 * rw.width);
+                var end1 = rwy.pointOffCenterline(rw.length,  0.5 * rw.width);
+                var end2 = rwy.pointOffCenterline(rw.length, -0.5 * rw.width);
+
+                icon_rw.setDataGeo
+                (
+                  [ canvas.Path.VG_MOVE_TO,
+                    canvas.Path.VG_LINE_TO,
+                    canvas.Path.VG_LINE_TO,
+                    canvas.Path.VG_LINE_TO,
+                    canvas.Path.VG_CLOSE_PATH ],
+                  [ beg1[0], beg1[1],
+                    beg2[0], beg2[1],
+                    end2[0], end2[1],
+                    end1[0], end1[1] ]
+                );
+               
+                if( rw.length / rw.width > 3 and !is_heli )
+                {
+                  # only runways which are much longer than wide are
+                  # real runways, otherwise it's probably a heliport.
+                  var icon_cl =
+                    me.grp_apt.createChild("path", "centerline")
+                              .setStrokeLineWidth(0.5)
+                              .setColor(1,1,1)
+                              .setStrokeDashArray([15, 10]);
+                 
+                  icon_cl.setDataGeo
+                  (
+                    [ canvas.Path.VG_MOVE_TO,
+                      canvas.Path.VG_LINE_TO ],
+                    [ beg_thr[0], beg_thr[1],
+                      end_thr[0], end_thr[1] ]
+                  );
+                 
+                  var icon_thr =
+                    me.grp_apt.createChild("path", "threshold")
+                              .setStrokeLineWidth(1.5)
+                              .setColor(1,1,1);
+                 
+                  icon_thr.setDataGeo
+                  (
+                    [ canvas.Path.VG_MOVE_TO,
+                      canvas.Path.VG_LINE_TO,
+                      canvas.Path.VG_MOVE_TO,
+                      canvas.Path.VG_LINE_TO ],
+                    [ beg_thr1[0], beg_thr1[1],
+                      beg_thr2[0], beg_thr2[1],
+                      end_thr1[0], end_thr1[1],
+                      end_thr2[0], end_thr2[1] ]
+                  );
+                }
+              }
+             
+              foreach(var park; me._apt.parking())
+              {
+                var icon_park =
+                  me.grp_apt.createChild("text")
+                            .setDrawMode( canvas.Text.ALIGNMENT
+                                        + canvas.Text.TEXT )
+                            .setText(park.name)
+                            .setFont("LiberationFonts/LiberationMono-Bold.ttf")
+                            .setGeoPosition(park.lat, park.lon)
+                            .setFontSize(15, 1.3);
+              }
+            }
+          };
+
diff --git a/gui/dialogs/airports.xml b/gui/dialogs/airports.xml
index 2fce235..25b6ad8 100644
--- a/gui/dialogs/airports.xml
+++ b/gui/dialogs/airports.xml
@@ -412,156 +412,6 @@
       
       
         <load><![CDATA[
-          var Runway = {
-            new: func(rwy)
-            {
-              return {
-                parents: [Runway],
-                rwy: rwy
-              };
-            },
-            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 AirportMap = {
-            new: func(apt)
-            {
-              return {
-                parents: [AirportMap],
-                _apt: apt
-              };
-            },
-            build: func(layer_runways)
-            {
-              var rws_done = {};
-
-              me.grp_apt = layer_runways.createChild("group", "apt-" ~ me._apt.id);
-              var selected_rwy = getprop("/sim/gui/dialogs/airports/selected-airport/rwy");         
-             
-              foreach(var rw; keys(me._apt.runways))
-              {
-                var is_heli = substr(rw, 0, 1) == "H";
-                var rw_dir = is_heli ? nil : int(substr(rw, 0, 2));
-               
-                var rw_rec = "";           
-                var thresh_rec = 0;
-                if( rw_dir != nil )
-                {
-                  rw_rec = sprintf("%02d", math.mod(rw_dir - 18, 36));
-                  if( size(rw) == 3 )
-                  {
-                    var map_rec = {
-                      "R": "L",
-                      "L": "R",
-                      "C": "C"
-                    };
-                    rw_rec ~= map_rec[substr(rw, 2)];
-                  }
-                 
-                  if( rws_done[rw_rec] != nil )
-                    continue;
-                 
-                  var rw_rec = me._apt.runways[rw_rec];
-                  if( rw_rec != nil )
-                    thresh_rec = rw_rec.threshold;
-                }
-
-                rws_done[rw] = 1;
-
-                rw = me._apt.runways[rw];
-                var icon_rw =
-                  me.grp_apt.createChild("path", "runway")
-                            .setStrokeLineWidth(0.5)
-                            .setColor(1.0,1.0,1.0)
-                            .setColorFill(0.2, 0.2, 0.2);
-
-                var rwy = Runway.new(rw);
-                var beg_thr  = rwy.pointOffCenterline(rw.threshold);
-                var beg_thr1 = rwy.pointOffCenterline(rw.threshold,  0.5 * rw.width);
-                var beg_thr2 = rwy.pointOffCenterline(rw.threshold, -0.5 * rw.width);
-                var beg1 = rwy.pointOffCenterline(0,  0.5 * rw.width);
-                var beg2 = rwy.pointOffCenterline(0, -0.5 * rw.width);
-               
-                var end_thr  = rwy.pointOffCenterline(rw.length - thresh_rec);
-                var end_thr1 = rwy.pointOffCenterline(rw.length - thresh_rec,  0.5 * rw.width);
-                var end_thr2 = rwy.pointOffCenterline(rw.length - thresh_rec, -0.5 * rw.width);
-                var end1 = rwy.pointOffCenterline(rw.length,  0.5 * rw.width);
-                var end2 = rwy.pointOffCenterline(rw.length, -0.5 * rw.width);
-
-                icon_rw.setDataGeo
-                (
-                  [ canvas.Path.VG_MOVE_TO,
-                    canvas.Path.VG_LINE_TO,
-                    canvas.Path.VG_LINE_TO,
-                    canvas.Path.VG_LINE_TO,
-                    canvas.Path.VG_CLOSE_PATH ],
-                  [ beg1[0], beg1[1],
-                    beg2[0], beg2[1],
-                    end2[0], end2[1],
-                    end1[0], end1[1] ]
-                );
-               
-                if( rw.length / rw.width > 3 and !is_heli )
-                {
-                  # only runways which are much longer than wide are
-                  # real runways, otherwise it's probably a heliport.
-                  var icon_cl =
-                    me.grp_apt.createChild("path", "centerline")
-                              .setStrokeLineWidth(0.5)
-                              .setColor(1,1,1)
-                              .setStrokeDashArray([15, 10]);
-                 
-                  icon_cl.setDataGeo
-                  (
-                    [ canvas.Path.VG_MOVE_TO,
-                      canvas.Path.VG_LINE_TO ],
-                    [ beg_thr[0], beg_thr[1],
-                      end_thr[0], end_thr[1] ]
-                  );
-                 
-                  var icon_thr =
-                    me.grp_apt.createChild("path", "threshold")
-                              .setStrokeLineWidth(1.5)
-                              .setColor(1,1,1);
-                 
-                  icon_thr.setDataGeo
-                  (
-                    [ canvas.Path.VG_MOVE_TO,
-                      canvas.Path.VG_LINE_TO,
-                      canvas.Path.VG_MOVE_TO,
-                      canvas.Path.VG_LINE_TO ],
-                    [ beg_thr1[0], beg_thr1[1],
-                      beg_thr2[0], beg_thr2[1],
-                      end_thr1[0], end_thr1[1],
-                      end_thr2[0], end_thr2[1] ]
-                  );
-                }
-              }
-             
-              foreach(var park; me._apt.parking())
-              {
-                var icon_park =
-                  me.grp_apt.createChild("text")
-                            .setDrawMode( canvas.Text.ALIGNMENT
-                                        + canvas.Text.TEXT )
-                            .setText(park.name)
-                            .setFont("LiberationFonts/LiberationMono-Bold.ttf")
-                            .setGeoPosition(park.lat, park.lon)
-                            .setFontSize(15, 1.3);
-              }
-            }
-          };
-
           var my_canvas = canvas.get(cmdarg());
           my_canvas.setColorBackground(0.2, 0.5, 0.2, 0.5);
 
@@ -588,8 +438,8 @@
           
             if (id != "") {
               var apt = airportinfo(id);
-              var airport = AirportMap.new(apt);
-              airport.build(layer_runways);
+              var airport = canvas.AirportMap.new(apt);
+              airport.build(layer_runways, "/sim/gui/dialogs/airports/selected-airport/rwy" );
 
               var pos = apt.tower();
               icon_tower.setGeoPosition(pos.lat, pos.lon);

Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11427
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby Hooray » Tue Sep 18, 2012 10:45 am

Regarding generalization, I'd suggest to proceed as follows:

  • create a "map" hash/class in map.nas - that shows up as "canvas.map" - instantiated via "canvas.map.new"
  • add a "map_layer" class which maps "chart layers" to canvas groups
  • implement custom layers (navaids, fixes, airports, route manager etc) by implementing the interface of a "map_layer"
  • add an "InstrumentOrWidget" class for maps that are "interactive" - i.e. controlled via GUI actions (properties) or cockpit hot spots (properties)
  • as can be seen in Stuart's code, we only need to link a handful of callbacks to certain actions

Next, I'd want to add methods so that the hard-coded symbols can be overridden using either SVG images or custom draw() callbacks.

This would allow us to easily add more features "as we go", i.e. when re-implementing:
  • the map dialog
  • the nav display
  • ATC-FS
  • Airport Diagram Generator

Also, it would probably be pretty simple to also add a canvas region to the route manager dialog, so that waypoints and airports are shown there too:
Image

Regarding label placement, this could be even implemented via a Nasal callback if each canvas element could write its bounding box dimensions to the property tree - but C++ space should be more efficient.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11427
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby TheTom » Tue Sep 18, 2012 11:29 am

I've now moved the helper classes to the canvas Nasal module. What also should be done is to assign correct names to the parking positions. Especially KSFO has loads of "Startup Location"s :) As parking positions are usually grouped together (eg. by a common letter or name part: GA [0-9]+, [A-Z] [0-9]+, etc.) it would make it easy to show just the whole group and only switch to the individual names once reaching a certain zoom level.
TheTom
 
Posts: 321
Joined: Sun Oct 09, 2011 10:20 am

Re: Using a canvas map in the GUI

Postby Hooray » Tue Sep 18, 2012 11:35 am

Basically, these "features" should all be optional "layers" (canvas groups) - so that we can instantiate a map with certain layers (i.e. allocated and put into a vector) and dynamically toggle them on/off.

For the route manager, I'd probably disable all the "startup positions" and instead just focus on airports, navaids & fixes - and of course the positions from the current route.

Once we have an abstract "Map_Layer" interface, we could implement each layer separately - possibly even in separate files, and maximize code sharing.
And then, we could also instantiate things in a pretty flexible fashion:

Code: Select all
Map.new = func (layers...) {
  var temp = {_layers:nil };
  foreach(var l; layers) append(temp._layers, l.new() );
  return temp;
}

var Foo = Map.new ( Layers.Airports, Layers.Navaids, Layers.Fixes );

Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11427
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby Hooray » Tue Sep 18, 2012 11:51 am

Looking at the code, there are some more things that could probably be moved to the canvas library, i.e. as part of a new "map.nas" module:

* getting/setting the map range, currently done via: map._node.getNode("range", 1).setDoubleValue(zoom);
* a method to easily zoom without setting properties manually (i.e. by factor or range)
* a method to update the center of the map (i.e. reposition), something like setCenter(lat,lon)
* an off-center mode?

Also, whenever someone is directly manipulating "low level" canvas properties, an API need becomes apparent ;-)
Code: Select all
             map._node.getNode("ref-lat", 1).setDoubleValue(apt.lat);
              map._node.getNode("ref-lon", 1).setDoubleValue(apt.lon);
              map._node.getNode("hdg", 1).setDoubleValue(0.0);



The "tower icon" would then probably also go to a separate layer, so that it can be separately toggled.

Each "map_layer" could then maintain a vector of drawables/elements, which are either file names pointing to SVG files or Nasal callbacks.
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11427
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby Hooray » Tue Sep 18, 2012 8:07 pm

Okay, according to the latest commits, Stuart has already implemented a number of the ideas discussed here, so I won't need to send my patch for review

However, even apart from the previously mentioned "leaking" vector data (after multiple subsequent airport lookups), there's still the issue of the updateZoom() callback being non-conditionally invoked via settimer(), so that it will never stop - it needs to be fixed to be terminated via a signal, or simply use the loopid method - otherwise, there will be more and more timers running, even after the dialog got closed.

Overall, I feel we should move such implementation details to helpers in map.nas - because GUI handling will probably be later on re-implemented using the new events subsystem. Otherwise, it is too easy for people to forget that timer callbacks need to properly terminated and listeners manually de-registered.

This is one of those cases were copy/paste errors propagate far too easily, even though they may already be fixed in the original place - which is why we should have a well localized place for such helpers, so that they don't develop their own "dynamics" ;-)

Personally, I'd suggest to add a helper hash "Map" with a handful of methods to help create new maps:
- new
- addLayer
- setRef
- setHdg
- setZoomRanges
- setZoomLevel

that should make it possible to implement most standard helpers as methods within the map.nas module
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11427
Joined: Tue Mar 25, 2008 8:40 am

Re: Using a canvas map in the GUI

Postby TheTom » Tue Sep 18, 2012 10:06 pm

Hooray wrote in Tue Sep 18, 2012 8:07 pm:However, even apart from the previously mentioned "leaking" vector data (after multiple subsequent airport lookups)

This is now fixed. For more details have a look at my post in the mailing list.

Hooray wrote in Tue Sep 18, 2012 11:51 am:Also, whenever someone is directly manipulating "low level" canvas properties, an API need becomes apparent ;-)

Yes :) Although it is now possible to use some convenience functions:
Code: Select all
map.setDouble("ref-lat", apt.lat);
map.setDouble("ref-lon", apt.lon);
map.setDouble("hdg", 0.0);
TheTom
 
Posts: 321
Joined: Sun Oct 09, 2011 10:20 am

Re: Using a canvas map in the GUI

Postby Hooray » Tue Sep 18, 2012 10:21 pm

I need to rebase my changes because of the work that you and Stuart and have committed now, but I would simply introduce a "Map" helper hash and add the various methods there.

Also, assuming that the "current position" is probably the default, we could default the position in various places, too:

map.nas
Code: Select all
var new = func return {parents:arg};
var map = {};
map.new = func new(map);
map.setRef(pos=nil) = func {
 var lat = getprop("/position/latitude-deg");
 var lon = getprop("/position/longitude-deg");
 if (pos) {
  typeof(pos) == "vector" or die("setRef() argument must be a vector: pos[0]=lat, pos[1]=lon");
  lat = pos[0]; lon = pos[1];
 }
 me.setDouble("ref-lat", lat);
 me.setDouble("ref-lon", lon);
}


Overall, there are now only a handful of opportunities left to make the code in the XML files shorter and to improve code reuse.
I'd suggest to also commit your map-canvas.xml file, so that we can implement the same changes.
That should also make it easier to increase code sharing over time.
And the map-canvas dialog will definitely need different groups for various features (airports, fixes, navaids)

PS: Good job regarding the listener issue ... so our initial guess wasn't totally wrong after all! :-)
Please don't send support requests by PM, instead post your questions on the forum so that all users can contribute and benefit
Thanks & all the best,
Hooray
Help write next month's newsletter !
pui2canvas | MapStructure | Canvas Development | Programming resources
Hooray
 
Posts: 11427
Joined: Tue Mar 25, 2008 8:40 am


Return to Canvas

Who is online

Users browsing this forum: No registered users and 1 guest